From 551a3018094d265580696ce0db09ea87a8cbe726 Mon Sep 17 00:00:00 2001 From: doe300 Date: Mon, 6 Jun 2022 11:34:51 +0200 Subject: [PATCH 1/6] Add support for collecting C/C++ code coverage via gcov --- src/CMakeLists.txt | 1 + src/config.cpp | 4 +- src/config.hpp | 1 + src/coverage.cpp | 122 +++++++++++++++++++++++++++++++++++++++++++++ src/coverage.hpp | 45 +++++++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/coverage.cpp create mode 100644 src/coverage.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 868462d..fda7ca3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ set(JUCI_SHARED_FILES commands.cpp config.cpp compile_commands.cpp + coverage.cpp ctags.cpp dispatcher.cpp documentation.cpp diff --git a/src/config.cpp b/src/config.cpp index 5e4e3b5..41804b6 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -189,6 +189,7 @@ void Config::read(const JSON &cfg) { project.cargo_command = project_json.string("cargo_command"); project.python_command = project_json.string("python_command"); project.markdown_command = project_json.string("markdown_command"); + project.gcov_command = project_json.string("gcov_command"); auto terminal_json = cfg.object("terminal"); terminal.history_size = terminal_json.integer("history_size", JSON::ParseOptions::accept_string); @@ -347,7 +348,8 @@ std::string Config::default_config() { "grep_command": "grep", "cargo_command": "cargo", "python_command": "python -u", - "markdown_command": "grip -b" + "markdown_command": "grip -b", + "gcov_command": "gcov -abcmtj" }, "keybindings": { "preferences": "comma", diff --git a/src/config.hpp b/src/config.hpp index 5d39cc0..5e70807 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -54,6 +54,7 @@ public: std::string cargo_command; std::string python_command; std::string markdown_command; + std::string gcov_command; }; class Source { diff --git a/src/coverage.cpp b/src/coverage.cpp new file mode 100644 index 0000000..129a75b --- /dev/null +++ b/src/coverage.cpp @@ -0,0 +1,122 @@ +#include "coverage.hpp" +#include "config.hpp" +#include "filesystem.hpp" +#include "json.hpp" +#include "terminal.hpp" +#include + +bool Coverage::BranchCoverage::operator<(const BranchCoverage &other) const noexcept { + if(count < other.count) { + return true; + } + if(count > other.count) { + return false; + } + // orders fall-through and throw branches to the end + if(is_throw < other.is_throw) { + return true; + } + if(is_throw > other.is_throw) { + return false; + } + return is_fallthrough < other.is_fallthrough; +} + +bool Coverage::LineCoverage::operator<(const LineCoverage &other) const noexcept { + if(line < other.line) { + return true; + } + if(line > other.line) { + return false; + } + if(count < other.count) { + return true; + } + if(count > other.count) { + return false; + } + return branches < other.branches; +} + +static bool is_not_covered(const Coverage::BranchCoverage &branch) { + return branch.count > 0; +} + +bool Coverage::LineCoverage::fully_covered() const noexcept { + return count > 0 && !has_unexecuted_statements && (branches.empty() || std::none_of(branches.begin(), branches.end(), is_not_covered)); +} + +bool Coverage::LineCoverage::partially_covered() const noexcept { + return count > 0 && (has_unexecuted_statements || (!branches.empty() && std::any_of(branches.begin(), branches.end(), is_not_covered))); +} + +bool Coverage::LineCoverage::not_covered() const noexcept { + return count == 0; +} + +static JSON run_gcov(const boost::filesystem::path &build_path, const boost::filesystem::path &object_file) { + auto command = Config::get().project.gcov_command + ' ' + filesystem::escape_argument(filesystem::get_short_path(object_file).string()); + std::stringstream stdin_stream, stdout_stream, stderr_stream; + auto exit_status = Terminal::get().process(stdin_stream, stdout_stream, command, build_path, &stderr_stream); + if(exit_status == 127) { + Terminal::get().print("\e[31mError\e[m: executable not found: " + command + "\n", true); + } + if(!stderr_stream.eof()) { + Terminal::get().async_print("\e[31mWarnings/Errors in gcov\e[m: " + stderr_stream.str(), true); + } + + return JSON(stdout_stream); +} + +static Coverage::BranchCoverage gcov_extract_branch(const JSON &branch_json) { + auto count = static_cast(branch_json.integer("count")); + auto is_fallthrough = branch_json.boolean_or("fallthrough", false); + auto is_throw = branch_json.boolean_or("throw", false); + return Coverage::BranchCoverage(count, is_fallthrough, is_throw); +} + +static Coverage::LineCoverage gcov_extract_line(const JSON &line_json) { + auto line = static_cast(line_json.integer("line_number")) - 1U /* in gcov, line numbers start at 1 */; + auto count = static_cast(line_json.integer("count")); + auto skipped_blocks = line_json.boolean_or("unexecuted_block", false); + + std::vector branches; + if(auto branch_json = line_json.array_optional("branches")) { + branches.reserve(branch_json->size()); + std::transform(branch_json->begin(), branch_json->end(), std::back_inserter(branches), gcov_extract_branch); + } + std::sort(branches.begin(), branches.end()); + + return Coverage::LineCoverage(line, count, skipped_blocks, std::move(branches)); +} + +static std::vector gcov_extract_lines(const std::vector &lines_json) { + std::vector lines; + lines.reserve(lines_json.size()); + std::transform(lines_json.begin(), lines_json.end(), std::back_inserter(lines), gcov_extract_line); + std::sort(lines.begin(), lines.end()); + return lines; +} + +static std::vector extract_gcov(JSON &&json, const boost::filesystem::path &source_file) { + auto files = json.array_optional("files"); + if(!files) { + return {}; + } + + for(const auto &file : *files) { + auto file_path = file.string_optional("file"); + auto lines = file.array_optional("lines"); + if(lines && file_path && *file_path == source_file) { + return gcov_extract_lines(*lines); + } + } + return {}; +} + +std::vector Coverage::analyze(const FileInfo &file_info) { + if(file_info.language_id == "cpp" || file_info.language_id == "c") { + return extract_gcov(run_gcov(file_info.build_path, file_info.object_path), file_info.source_path); + } + return {}; +} diff --git a/src/coverage.hpp b/src/coverage.hpp new file mode 100644 index 0000000..626920d --- /dev/null +++ b/src/coverage.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "boost/filesystem.hpp" +#include + +namespace Coverage { + + class BranchCoverage { + public: + explicit BranchCoverage(unsigned long num_calls, bool fallthrough = false, bool exception = false) noexcept : count(num_calls), is_fallthrough(fallthrough), is_throw(exception) {} + + bool operator<(const BranchCoverage &other) const noexcept; + + unsigned long count; + bool is_fallthrough; + bool is_throw; + }; + + class LineCoverage { + public: + LineCoverage(unsigned long code_line, unsigned long num_calls, bool skipped_statements = false, std::vector &&jumps = {}) noexcept : line(code_line), count(num_calls), has_unexecuted_statements(skipped_statements), branches(std::move(jumps)) {} + + bool operator<(const LineCoverage &other) const noexcept; + + bool fully_covered() const noexcept; + bool partially_covered() const noexcept; + bool not_covered() const noexcept; + + unsigned long line; + unsigned long count; + bool has_unexecuted_statements; + + std::vector branches; + }; + + + struct FileInfo { + boost::filesystem::path source_path; + boost::filesystem::path object_path; + boost::filesystem::path build_path; + std::string language_id; + }; + + std::vector analyze(const FileInfo &file_info); +} // namespace Coverage From a46e900379cc4eb2ff81b3534b5264dff4899011 Mon Sep 17 00:00:00 2001 From: doe300 Date: Mon, 6 Jun 2022 11:39:00 +0200 Subject: [PATCH 2/6] Add CoverageView to display line/branch coverage in the editor side-bar (gutter) --- src/CMakeLists.txt | 1 + src/config.cpp | 1 + src/menu.cpp | 6 ++ src/source.cpp | 3 +- src/source.hpp | 3 +- src/source_clang.cpp | 30 +++++++++ src/source_coverage.cpp | 141 ++++++++++++++++++++++++++++++++++++++++ src/source_coverage.hpp | 34 ++++++++++ src/window.cpp | 7 ++ 9 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/source_coverage.cpp create mode 100644 src/source_coverage.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fda7ca3..66d7d1c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ set(JUCI_SHARED_FILES source.cpp source_base.cpp source_clang.cpp + source_coverage.cpp source_diff.cpp source_generic.cpp source_language_protocol.cpp diff --git a/src/config.cpp b/src/config.cpp index 41804b6..97aaf10 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -408,6 +408,7 @@ std::string Config::default_config() { "source_implement_method": "m", "source_goto_next_diagnostic": "e", "source_apply_fix_its": "space", + "source_toggle_coverage": "c", "project_set_run_arguments": "", "project_compile_and_run": "Return", "project_compile": "Return", diff --git a/src/menu.cpp b/src/menu.cpp index 2f819a7..e8eb89e 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -399,6 +399,12 @@ const Glib::ustring menu_xml = R"RAW( app.source_apply_fix_its +
+ + _Toggle _Coverage + app.source_toggle_coverage + +
diff --git a/src/source.cpp b/src/source.cpp index 573f135..02ac5ff 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -142,7 +142,7 @@ std::string Source::FixIt::string(BaseView &view) { std::set Source::View::non_deleted_views; std::set Source::View::views; -Source::View::View(const boost::filesystem::path &file_path, const Glib::RefPtr &language, bool is_generic_view) : BaseView(file_path, language), SpellCheckView(file_path, language), DiffView(file_path, language) { +Source::View::View(const boost::filesystem::path &file_path, const Glib::RefPtr &language, bool is_generic_view) : BaseView(file_path, language), SpellCheckView(file_path, language), DiffView(file_path, language), CoverageView(file_path, language) { non_deleted_views.emplace(this); views.emplace(this); @@ -463,6 +463,7 @@ bool Source::View::save() { void Source::View::configure() { SpellCheckView::configure(); DiffView::configure(); + CoverageView::configure(); if(Config::get().source.style.size() > 0) { auto scheme = StyleSchemeManager::get_default()->get_scheme(Config::get().source.style); diff --git a/src/source.hpp b/src/source.hpp index 2a76e7a..038046f 100644 --- a/src/source.hpp +++ b/src/source.hpp @@ -1,5 +1,6 @@ #pragma once #include "process.hpp" +#include "source_coverage.hpp" #include "source_diff.hpp" #include "source_spellcheck.hpp" #include "tooltips.hpp" @@ -72,7 +73,7 @@ namespace Source { } }; - class View : public SpellCheckView, public DiffView { + class View : public SpellCheckView, public DiffView, public CoverageView { public: static std::set non_deleted_views; static std::set views; diff --git a/src/source_clang.cpp b/src/source_clang.cpp index 9550ae1..f71deb0 100644 --- a/src/source_clang.cpp +++ b/src/source_clang.cpp @@ -2003,6 +2003,36 @@ Source::ClangViewRefactor::ClangViewRefactor(const boost::filesystem::path &file return std::tuple(Source::Offset(), "", 0); } }; + + get_coverage = [this]() { + auto build = Project::Build::create(this->file_path); + if(build->project_path.empty()) { + Info::get().print(this->file_path.filename().string() + ": could not find a supported build system"); + return std::vector{}; + } + build->update_default(); + CompileCommands commands(build->get_default_path()); + boost::filesystem::path object_file; + for(const auto &command : commands.commands) { + if(command.file == this->file_path) { + auto values = command.parameter_values("-o"); + if(!values.empty()) { + object_file = command.directory / values.front(); + break; + } + } + } + if(object_file.empty()) { + Info::get().print(this->file_path.filename().string() + ": could not find the C/C++ object file"); + return std::vector{}; + } + + auto result = Coverage::analyze(Coverage::FileInfo{this->file_path, object_file, build->get_default_path(), this->language_id}); + if(result.empty()) { + Info::get().print(this->file_path.filename().string() + ": no supported coverage information found"); + } + return result; + }; } Source::ClangViewRefactor::Identifier Source::ClangViewRefactor::get_identifier() { diff --git a/src/source_coverage.cpp b/src/source_coverage.cpp new file mode 100644 index 0000000..1eb2975 --- /dev/null +++ b/src/source_coverage.cpp @@ -0,0 +1,141 @@ +#include "source_coverage.hpp" +#include +#include + +Source::CoverageView::Renderer::Renderer() : Gsv::GutterRendererText() { + set_padding(4, 0); +} + +Source::CoverageView::CoverageView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : BaseView(file_path, language), renderer(new Renderer()) {} + +Source::CoverageView::~CoverageView() { + get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->remove(renderer.get()); +} + +void Source::CoverageView::configure() { + // Set colors + auto &yellow = renderer->yellow; + auto &red = renderer->red; + auto &green = renderer->green; + auto &transparent = renderer->transparent; + auto normal_color = get_style_context()->get_color(Gtk::StateFlags::STATE_FLAG_NORMAL); + auto light_theme = (normal_color.get_red() + normal_color.get_green() + normal_color.get_blue()) / 3 < 0.5; + yellow.set_rgba(1.0, 1.0, 0.2); + double factor = light_theme ? 0.85 : 0.5; + yellow.set_red(normal_color.get_red() + factor * (yellow.get_red() - normal_color.get_red())); + yellow.set_green(normal_color.get_green() + factor * (yellow.get_green() - normal_color.get_green())); + yellow.set_blue(normal_color.get_blue() + factor * (yellow.get_blue() - normal_color.get_blue())); + red.set_rgba(1.0, 0.0, 0.0); + factor = light_theme ? 0.8 : 0.35; + red.set_red(normal_color.get_red() + factor * (red.get_red() - normal_color.get_red())); + red.set_green(normal_color.get_green() + factor * (red.get_green() - normal_color.get_green())); + red.set_blue(normal_color.get_blue() + factor * (red.get_blue() - normal_color.get_blue())); + green.set_rgba(0.0, 1.0, 0.0); + factor = light_theme ? 0.7 : 0.4; + green.set_red(normal_color.get_red() + factor * (green.get_red() - normal_color.get_red())); + green.set_green(normal_color.get_green() + factor * (green.get_green() - normal_color.get_green())); + green.set_blue(normal_color.get_blue() + factor * (green.get_blue() - normal_color.get_blue())); + transparent.set_rgba(1.0, 1.0, 1.0, 1.0); + + // Style gutter + renderer->set_alignment_mode(Gsv::GutterRendererAlignmentMode::GUTTER_RENDERER_ALIGNMENT_MODE_FIRST); + renderer->set_alignment(1.0, -1); + renderer->set_padding(3, -1); + + // Connect gutter renderer signals + renderer->signal_query_data().connect([this](const Gtk::TextIter &start, const Gtk::TextIter &end, Gsv::GutterRendererState state) { + update_gutter(find_coverage(start.get_line())); + }); + renderer->signal_query_activatable().connect([this](const Gtk::TextIter &, const Gdk::Rectangle &, GdkEvent *) { + return !line_coverage.empty(); + }); + + renderer->signal_query_tooltip().connect([this](const Gtk::TextIter &iter, const Gdk::Rectangle &area, int x, int y, const Glib::RefPtr &tooltip) { + if(const auto *entry = find_coverage(iter.get_line())) { + tooltip->set_text(update_tooltip(*entry)); + return true; + } + return false; + }); + + get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->insert(renderer.get(), -50); + renderer->set_visible(false); +} + +void Source::CoverageView::toggle_coverage() { + if(!line_coverage.empty()) { + update_coverage({}); + } + else if(get_coverage) { + update_coverage(get_coverage()); + } +} + +void Source::CoverageView::update_coverage(std::vector &&coverage) { + line_coverage = std::move(coverage); + unsigned long max_line_count = 0; + for(const auto &line : line_coverage) { + max_line_count = std::max(max_line_count, line.count); + } + + if(line_coverage.empty()) { + renderer->set_visible(false); + } + else { + int width, height; + renderer->measure(std::to_string(max_line_count), width, height); + renderer->set_visible(true); + renderer->set_size(width); + } + + renderer->queue_draw(); +} + +const Coverage::LineCoverage *Source::CoverageView::find_coverage(int line) { + if(line_coverage.empty()) { + return nullptr; + } + auto it = std::lower_bound(line_coverage.begin(), line_coverage.end(), line, [](const Coverage::LineCoverage &entry, int line) { + return entry.line < static_cast(line); + }); + if(it != line_coverage.end() && it->line == static_cast(line)) { + return &(*it); + } + + return nullptr; +} + +void Source::CoverageView::update_gutter(const Coverage::LineCoverage *line_coverage) { + if(!line_coverage) { + renderer->set_text(""); + renderer->set_background(renderer->transparent); + return; + } + + renderer->set_text(std::to_string(line_coverage->count)); + if(line_coverage->fully_covered()) { + renderer->set_background(renderer->green); + } + else if(line_coverage->partially_covered()) { + renderer->set_background(renderer->yellow); + } + else { + renderer->set_background(renderer->red); + } +} + +std::string Source::CoverageView::update_tooltip(const Coverage::LineCoverage &line_coverage) { + std::stringstream ss; + ss << "Line executed " << line_coverage.count << " times"; + if(line_coverage.count > 0 && line_coverage.has_unexecuted_statements) { + ss << " (with unexecuted statements)"; + } + + if(!line_coverage.branches.empty()) { + ss << ", with branches:"; + for(const auto &branch : line_coverage.branches) { + ss << "\n- " << (branch.is_fallthrough ? "Fall-through " : "") << (branch.is_throw ? "Exceptional " : "") << "Branch executed " << branch.count << " times"; + } + } + return ss.str(); +} diff --git a/src/source_coverage.hpp b/src/source_coverage.hpp new file mode 100644 index 0000000..76453c3 --- /dev/null +++ b/src/source_coverage.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "coverage.hpp" +#include "source_base.hpp" + +namespace Source { + class CoverageView : virtual public Source::BaseView { + + class Renderer : public Gsv::GutterRendererText { + public: + Renderer(); + + Gdk::RGBA yellow, red, green, transparent; + }; + + public: + CoverageView(const boost::filesystem::path &file_path, const Glib::RefPtr &language); + ~CoverageView() override; + + void configure() override; + + std::function()> get_coverage; + void toggle_coverage(); + + private: + std::unique_ptr renderer; + std::vector line_coverage; + + void update_coverage(std::vector &&coverage); + void update_gutter(const Coverage::LineCoverage *line_coverage); + std::string update_tooltip(const Coverage::LineCoverage &line_coverage); + + const Coverage::LineCoverage *find_coverage(int line); + }; +} // namespace Source diff --git a/src/window.cpp b/src/window.cpp index 718a746..bf6d4c1 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1376,6 +1376,12 @@ void Window::set_menu_actions() { } }); + menu.add_action("source_toggle_coverage", []() { + if(auto view = Notebook::get().get_current_view()) { + view->toggle_coverage(); + } + }); + menu.add_action("project_set_run_arguments", []() { auto project = Project::create(); auto run_arguments = project->get_run_arguments(); @@ -1804,6 +1810,7 @@ void Window::set_menu_actions() { menu.actions["source_implement_method"]->set_enabled(view && view->get_method); menu.actions["source_goto_next_diagnostic"]->set_enabled(view && view->goto_next_diagnostic); menu.actions["source_apply_fix_its"]->set_enabled(view && view->get_fix_its); + menu.actions["source_toggle_coverage"]->set_enabled(view && view->get_coverage); #ifdef JUCI_ENABLE_DEBUG Project::debug_activate_menu_items(); #endif From 4eba732576927a876dfa4e334a512d260c093659 Mon Sep 17 00:00:00 2001 From: doe300 Date: Sat, 11 Jun 2022 11:51:02 +0200 Subject: [PATCH 3/6] Generalize code coverage support for all views --- src/source_clang.cpp | 30 ------------------------------ src/source_coverage.cpp | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/source_clang.cpp b/src/source_clang.cpp index f71deb0..9550ae1 100644 --- a/src/source_clang.cpp +++ b/src/source_clang.cpp @@ -2003,36 +2003,6 @@ Source::ClangViewRefactor::ClangViewRefactor(const boost::filesystem::path &file return std::tuple(Source::Offset(), "", 0); } }; - - get_coverage = [this]() { - auto build = Project::Build::create(this->file_path); - if(build->project_path.empty()) { - Info::get().print(this->file_path.filename().string() + ": could not find a supported build system"); - return std::vector{}; - } - build->update_default(); - CompileCommands commands(build->get_default_path()); - boost::filesystem::path object_file; - for(const auto &command : commands.commands) { - if(command.file == this->file_path) { - auto values = command.parameter_values("-o"); - if(!values.empty()) { - object_file = command.directory / values.front(); - break; - } - } - } - if(object_file.empty()) { - Info::get().print(this->file_path.filename().string() + ": could not find the C/C++ object file"); - return std::vector{}; - } - - auto result = Coverage::analyze(Coverage::FileInfo{this->file_path, object_file, build->get_default_path(), this->language_id}); - if(result.empty()) { - Info::get().print(this->file_path.filename().string() + ": no supported coverage information found"); - } - return result; - }; } Source::ClangViewRefactor::Identifier Source::ClangViewRefactor::get_identifier() { diff --git a/src/source_coverage.cpp b/src/source_coverage.cpp index 1eb2975..9c2afd7 100644 --- a/src/source_coverage.cpp +++ b/src/source_coverage.cpp @@ -1,4 +1,5 @@ #include "source_coverage.hpp" +#include "info.hpp" #include #include @@ -6,7 +7,22 @@ Source::CoverageView::Renderer::Renderer() : Gsv::GutterRendererText() { set_padding(4, 0); } -Source::CoverageView::CoverageView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : BaseView(file_path, language), renderer(new Renderer()) {} +Source::CoverageView::CoverageView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : BaseView(file_path, language), renderer(new Renderer()) { + get_coverage = [this]() { + auto build = Project::Build::create(this->file_path); + if(build->project_path.empty()) { + Info::get().print(this->file_path.filename().string() + ": could not find a supported build system"); + return std::vector{}; + } + build->update_default(); + + auto result = Coverage::analyze(*build, this->file_path, this->language_id); + if(result.empty()) { + Info::get().print(this->file_path.filename().string() + ": no supported coverage information found"); + } + return result; + }; +} Source::CoverageView::~CoverageView() { get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->remove(renderer.get()); From d1fd1b7a8e2384072957ed544f25ce7ba8224c46 Mon Sep 17 00:00:00 2001 From: doe300 Date: Sat, 11 Jun 2022 11:52:34 +0200 Subject: [PATCH 4/6] Add support for Rust coverage via tarpaulin --- src/coverage.cpp | 73 ++++++++++++++++++++++++++++++++++++++++++++++-- src/coverage.hpp | 11 ++------ 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/coverage.cpp b/src/coverage.cpp index 129a75b..0068b7b 100644 --- a/src/coverage.cpp +++ b/src/coverage.cpp @@ -1,4 +1,5 @@ #include "coverage.hpp" +#include "compile_commands.hpp" #include "config.hpp" #include "filesystem.hpp" #include "json.hpp" @@ -114,9 +115,75 @@ static std::vector extract_gcov(JSON &&json, const boost return {}; } -std::vector Coverage::analyze(const FileInfo &file_info) { - if(file_info.language_id == "cpp" || file_info.language_id == "c") { - return extract_gcov(run_gcov(file_info.build_path, file_info.object_path), file_info.source_path); +static Coverage::LineCoverage tarpaulin_extract_line(const JSON &line_json) { + auto line = static_cast(line_json.integer("line")) - 1U /* in tarpaulin, line numbers start at 1 */; + auto stats = line_json.object_optional("stats"); + if(!stats) { + return Coverage::LineCoverage(line, 0); + } + + auto count = static_cast(stats->integer_optional("Line").value_or(0)); + // branch and conditionally coverage are not yet implemented in tarpaulin (as of version 0.20.1 + return Coverage::LineCoverage(line, count); +} + +static std::vector tarpaulin_extract_lines(const std::vector &lines_json) { + std::vector lines; + lines.reserve(lines_json.size()); + std::transform(lines_json.begin(), lines_json.end(), std::back_inserter(lines), tarpaulin_extract_line); + std::sort(lines.begin(), lines.end()); + return lines; +} + +static std::vector extract_tarpaulin(JSON &&json, const boost::filesystem::path &source_file) { + auto traces = json.object_optional("traces"); + if(!traces) { + return {}; + } + + auto file_traces = traces->array_optional(source_file.string()); + if(!file_traces) { + return {}; + } + + return tarpaulin_extract_lines(*file_traces); +} + +std::vector Coverage::analyze(Project::Build &build, const boost::filesystem::path &file_path, const std::string &language_id) { + if(language_id == "cpp" || language_id == "c") { + CompileCommands commands(build.get_default_path()); + boost::filesystem::path object_file; + for(const auto &command : commands.commands) { + if(command.file == file_path) { + auto values = command.parameter_values("-o"); + if(!values.empty()) { + object_file = command.directory / values.front(); + break; + } + } + } + if(object_file.empty()) { + Terminal::get().async_print(file_path.filename().string() + ": could not find the C/C++ object file", true); + return std::vector{}; + } + + return extract_gcov(run_gcov(build.get_default_path(), object_file), file_path); + } + + if(language_id == "rust" && dynamic_cast(&build)) { + auto tarpaulin_folder = filesystem::get_canonical_path(build.get_default_path() / ".." / "tarpaulin"); + if(!boost::filesystem::exists(tarpaulin_folder) || !boost::filesystem::is_directory(tarpaulin_folder)) { + Terminal::get().async_print("Directory '" + tarpaulin_folder.string() + "' does not exist, you may need to generate the coverage report via: cargo tarpaulin", true); + return std::vector{}; + } + for(const auto &file : boost::filesystem::directory_iterator(tarpaulin_folder)) { + if(boost::filesystem::is_regular(file.path()) && file.path().extension().string() == ".json") { + return extract_tarpaulin(JSON(file.path()), file_path); + } + } + + Terminal::get().async_print("No JSON coverage file found in '" + tarpaulin_folder.string() + "', you may need to generate the coverage report via: cargo tarpaulin", true); + return std::vector{}; } return {}; } diff --git a/src/coverage.hpp b/src/coverage.hpp index 626920d..db96374 100644 --- a/src/coverage.hpp +++ b/src/coverage.hpp @@ -1,6 +1,7 @@ #pragma once #include "boost/filesystem.hpp" +#include "project_build.hpp" #include namespace Coverage { @@ -33,13 +34,5 @@ namespace Coverage { std::vector branches; }; - - struct FileInfo { - boost::filesystem::path source_path; - boost::filesystem::path object_path; - boost::filesystem::path build_path; - std::string language_id; - }; - - std::vector analyze(const FileInfo &file_info); + std::vector analyze(Project::Build &build, const boost::filesystem::path &file_path, const std::string &language_id); } // namespace Coverage From 771722904ea317088ab55b63caa7321da01c3434 Mon Sep 17 00:00:00 2001 From: doe300 Date: Thu, 27 Jul 2023 18:49:31 +0200 Subject: [PATCH 5/6] Adapted to upstream changes after rebase --- src/coverage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coverage.cpp b/src/coverage.cpp index 0068b7b..e41e497 100644 --- a/src/coverage.cpp +++ b/src/coverage.cpp @@ -155,7 +155,7 @@ std::vector Coverage::analyze(Project::Build &build, con boost::filesystem::path object_file; for(const auto &command : commands.commands) { if(command.file == file_path) { - auto values = command.parameter_values("-o"); + auto values = command.get_argument_values("-o"); if(!values.empty()) { object_file = command.directory / values.front(); break; From 5b6149ad1e78783f40b7263de0fa9ee7dcb94525 Mon Sep 17 00:00:00 2001 From: doe300 Date: Sun, 12 Nov 2023 12:28:00 +0100 Subject: [PATCH 6/6] Improvements for coverage display and error handling --- src/coverage.cpp | 33 ++++++++++++++++++++++++++++----- src/coverage.hpp | 2 ++ src/source_coverage.cpp | 4 ++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/coverage.cpp b/src/coverage.cpp index e41e497..8b4b230 100644 --- a/src/coverage.cpp +++ b/src/coverage.cpp @@ -40,7 +40,7 @@ bool Coverage::LineCoverage::operator<(const LineCoverage &other) const noexcept } static bool is_not_covered(const Coverage::BranchCoverage &branch) { - return branch.count > 0; + return branch.count == 0; } bool Coverage::LineCoverage::fully_covered() const noexcept { @@ -55,6 +55,21 @@ bool Coverage::LineCoverage::not_covered() const noexcept { return count == 0; } +std::string Coverage::LineCoverage::branch_type(std::size_t branchIndex) const { + // If there are only 2 branches, one "exceptional" and the other "fall-through", report the "fall-through" branch as default. + if(branches.size() == 2 && branches[branchIndex].is_fallthrough && branches[branchIndex ^ 1].is_throw) { + return "Default "; + } + std::string type = ""; + if(branches[branchIndex].is_fallthrough) { + type += "Fall-through "; + } + if(branches[branchIndex].is_throw) { + type += "Exceptional "; + } + return type; +} + static JSON run_gcov(const boost::filesystem::path &build_path, const boost::filesystem::path &object_file) { auto command = Config::get().project.gcov_command + ' ' + filesystem::escape_argument(filesystem::get_short_path(object_file).string()); std::stringstream stdin_stream, stdout_stream, stderr_stream; @@ -63,7 +78,10 @@ static JSON run_gcov(const boost::filesystem::path &build_path, const boost::fil Terminal::get().print("\e[31mError\e[m: executable not found: " + command + "\n", true); } if(!stderr_stream.eof()) { - Terminal::get().async_print("\e[31mWarnings/Errors in gcov\e[m: " + stderr_stream.str(), true); + auto errorOutput = stderr_stream.str(); + if(!errorOutput.empty()) { + Terminal::get().async_print("\e[31mWarnings/Errors in gcov\e[m: " + errorOutput + "\n", true); + } } return JSON(stdout_stream); @@ -163,7 +181,12 @@ std::vector Coverage::analyze(Project::Build &build, con } } if(object_file.empty()) { - Terminal::get().async_print(file_path.filename().string() + ": could not find the C/C++ object file", true); + if(dynamic_cast(&build)) { + Terminal::get().async_print(file_path.filename().string() + ": could not find the C/C++ object file, you may need to enable the CMake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON switch\n", true); + } + else { + Terminal::get().async_print(file_path.filename().string() + ": could not find the C/C++ object file\n", true); + } return std::vector{}; } @@ -173,7 +196,7 @@ std::vector Coverage::analyze(Project::Build &build, con if(language_id == "rust" && dynamic_cast(&build)) { auto tarpaulin_folder = filesystem::get_canonical_path(build.get_default_path() / ".." / "tarpaulin"); if(!boost::filesystem::exists(tarpaulin_folder) || !boost::filesystem::is_directory(tarpaulin_folder)) { - Terminal::get().async_print("Directory '" + tarpaulin_folder.string() + "' does not exist, you may need to generate the coverage report via: cargo tarpaulin", true); + Terminal::get().async_print("Directory '" + tarpaulin_folder.string() + "' does not exist, you may need to generate the coverage report via: cargo tarpaulin\n", true); return std::vector{}; } for(const auto &file : boost::filesystem::directory_iterator(tarpaulin_folder)) { @@ -182,7 +205,7 @@ std::vector Coverage::analyze(Project::Build &build, con } } - Terminal::get().async_print("No JSON coverage file found in '" + tarpaulin_folder.string() + "', you may need to generate the coverage report via: cargo tarpaulin", true); + Terminal::get().async_print("No JSON coverage file found in '" + tarpaulin_folder.string() + "', you may need to generate the coverage report via: cargo tarpaulin\n", true); return std::vector{}; } return {}; diff --git a/src/coverage.hpp b/src/coverage.hpp index db96374..940d173 100644 --- a/src/coverage.hpp +++ b/src/coverage.hpp @@ -27,6 +27,8 @@ namespace Coverage { bool partially_covered() const noexcept; bool not_covered() const noexcept; + std::string branch_type(std::size_t branchIndex) const; + unsigned long line; unsigned long count; bool has_unexecuted_statements; diff --git a/src/source_coverage.cpp b/src/source_coverage.cpp index 9c2afd7..6640b95 100644 --- a/src/source_coverage.cpp +++ b/src/source_coverage.cpp @@ -149,8 +149,8 @@ std::string Source::CoverageView::update_tooltip(const Coverage::LineCoverage &l if(!line_coverage.branches.empty()) { ss << ", with branches:"; - for(const auto &branch : line_coverage.branches) { - ss << "\n- " << (branch.is_fallthrough ? "Fall-through " : "") << (branch.is_throw ? "Exceptional " : "") << "Branch executed " << branch.count << " times"; + for(std::size_t i = 0; i < line_coverage.branches.size(); ++i) { + ss << "\n- " << line_coverage.branch_type(i) << "Branch executed " << line_coverage.branches[i].count << " times"; } } return ss.str();