Browse Source

Merge branch 'code_coverage' into 'master'

Display code coverage

See merge request cppit/jucipp!413
merge-requests/413/merge
doe300 2 years ago
parent
commit
3475d26837
  1. 2
      src/CMakeLists.txt
  2. 5
      src/config.cpp
  3. 1
      src/config.hpp
  4. 212
      src/coverage.cpp
  5. 40
      src/coverage.hpp
  6. 6
      src/menu.cpp
  7. 3
      src/source.cpp
  8. 3
      src/source.hpp
  9. 157
      src/source_coverage.cpp
  10. 34
      src/source_coverage.hpp
  11. 7
      src/window.cpp

2
src/CMakeLists.txt

@ -5,6 +5,7 @@ set(JUCI_SHARED_FILES
commands.cpp commands.cpp
config.cpp config.cpp
compile_commands.cpp compile_commands.cpp
coverage.cpp
ctags.cpp ctags.cpp
dispatcher.cpp dispatcher.cpp
documentation.cpp documentation.cpp
@ -19,6 +20,7 @@ set(JUCI_SHARED_FILES
source.cpp source.cpp
source_base.cpp source_base.cpp
source_clang.cpp source_clang.cpp
source_coverage.cpp
source_diff.cpp source_diff.cpp
source_generic.cpp source_generic.cpp
source_language_protocol.cpp source_language_protocol.cpp

5
src/config.cpp

@ -189,6 +189,7 @@ void Config::read(const JSON &cfg) {
project.cargo_command = project_json.string("cargo_command"); project.cargo_command = project_json.string("cargo_command");
project.python_command = project_json.string("python_command"); project.python_command = project_json.string("python_command");
project.markdown_command = project_json.string("markdown_command"); project.markdown_command = project_json.string("markdown_command");
project.gcov_command = project_json.string("gcov_command");
auto terminal_json = cfg.object("terminal"); auto terminal_json = cfg.object("terminal");
terminal.history_size = terminal_json.integer("history_size", JSON::ParseOptions::accept_string); terminal.history_size = terminal_json.integer("history_size", JSON::ParseOptions::accept_string);
@ -347,7 +348,8 @@ std::string Config::default_config() {
"grep_command": "grep", "grep_command": "grep",
"cargo_command": "cargo", "cargo_command": "cargo",
"python_command": "python -u", "python_command": "python -u",
"markdown_command": "grip -b" "markdown_command": "grip -b",
"gcov_command": "gcov -abcmtj"
}, },
"keybindings": { "keybindings": {
"preferences": "<primary>comma", "preferences": "<primary>comma",
@ -406,6 +408,7 @@ std::string Config::default_config() {
"source_implement_method": "<primary><shift>m", "source_implement_method": "<primary><shift>m",
"source_goto_next_diagnostic": "<primary>e", "source_goto_next_diagnostic": "<primary>e",
"source_apply_fix_its": "<control>space", "source_apply_fix_its": "<control>space",
"source_toggle_coverage": "<primary><shift>c",
"project_set_run_arguments": "", "project_set_run_arguments": "",
"project_compile_and_run": "<primary>Return", "project_compile_and_run": "<primary>Return",
"project_compile": "<primary><shift>Return", "project_compile": "<primary><shift>Return",

1
src/config.hpp

@ -54,6 +54,7 @@ public:
std::string cargo_command; std::string cargo_command;
std::string python_command; std::string python_command;
std::string markdown_command; std::string markdown_command;
std::string gcov_command;
}; };
class Source { class Source {

212
src/coverage.cpp

@ -0,0 +1,212 @@
#include "coverage.hpp"
#include "compile_commands.hpp"
#include "config.hpp"
#include "filesystem.hpp"
#include "json.hpp"
#include "terminal.hpp"
#include <algorithm>
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;
}
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;
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()) {
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);
}
static Coverage::BranchCoverage gcov_extract_branch(const JSON &branch_json) {
auto count = static_cast<unsigned long>(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<unsigned long>(line_json.integer("line_number")) - 1U /* in gcov, line numbers start at 1 */;
auto count = static_cast<unsigned long>(line_json.integer("count"));
auto skipped_blocks = line_json.boolean_or("unexecuted_block", false);
std::vector<Coverage::BranchCoverage> 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<Coverage::LineCoverage> gcov_extract_lines(const std::vector<JSON> &lines_json) {
std::vector<Coverage::LineCoverage> 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<Coverage::LineCoverage> 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 {};
}
static Coverage::LineCoverage tarpaulin_extract_line(const JSON &line_json) {
auto line = static_cast<unsigned long>(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<unsigned long>(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<Coverage::LineCoverage> tarpaulin_extract_lines(const std::vector<JSON> &lines_json) {
std::vector<Coverage::LineCoverage> 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<Coverage::LineCoverage> 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::LineCoverage> 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.get_argument_values("-o");
if(!values.empty()) {
object_file = command.directory / values.front();
break;
}
}
}
if(object_file.empty()) {
if(dynamic_cast<const Project::CMakeBuild *>(&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<Coverage::LineCoverage>{};
}
return extract_gcov(run_gcov(build.get_default_path(), object_file), file_path);
}
if(language_id == "rust" && dynamic_cast<Project::CargoBuild *>(&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\n", true);
return std::vector<Coverage::LineCoverage>{};
}
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\n", true);
return std::vector<Coverage::LineCoverage>{};
}
return {};
}

40
src/coverage.hpp

@ -0,0 +1,40 @@
#pragma once
#include "boost/filesystem.hpp"
#include "project_build.hpp"
#include <vector>
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<BranchCoverage> &&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;
std::string branch_type(std::size_t branchIndex) const;
unsigned long line;
unsigned long count;
bool has_unexecuted_statements;
std::vector<BranchCoverage> branches;
};
std::vector<LineCoverage> analyze(Project::Build &build, const boost::filesystem::path &file_path, const std::string &language_id);
} // namespace Coverage

6
src/menu.cpp

@ -399,6 +399,12 @@ const Glib::ustring menu_xml = R"RAW(<interface>
<attribute name='action'>app.source_apply_fix_its</attribute> <attribute name='action'>app.source_apply_fix_its</attribute>
</item> </item>
</section> </section>
<section>
<item>
<attribute name='label' translatable='yes'>_Toggle _Coverage</attribute>
<attribute name='action'>app.source_toggle_coverage</attribute>
</item>
</section>
</submenu> </submenu>
<submenu> <submenu>

3
src/source.cpp

@ -142,7 +142,7 @@ std::string Source::FixIt::string(BaseView &view) {
std::set<Source::View *> Source::View::non_deleted_views; std::set<Source::View *> Source::View::non_deleted_views;
std::set<Source::View *> Source::View::views; std::set<Source::View *> Source::View::views;
Source::View::View(const boost::filesystem::path &file_path, const Glib::RefPtr<Gsv::Language> &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<Gsv::Language> &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); non_deleted_views.emplace(this);
views.emplace(this); views.emplace(this);
@ -463,6 +463,7 @@ bool Source::View::save() {
void Source::View::configure() { void Source::View::configure() {
SpellCheckView::configure(); SpellCheckView::configure();
DiffView::configure(); DiffView::configure();
CoverageView::configure();
if(Config::get().source.style.size() > 0) { if(Config::get().source.style.size() > 0) {
auto scheme = StyleSchemeManager::get_default()->get_scheme(Config::get().source.style); auto scheme = StyleSchemeManager::get_default()->get_scheme(Config::get().source.style);

3
src/source.hpp

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "process.hpp" #include "process.hpp"
#include "source_coverage.hpp"
#include "source_diff.hpp" #include "source_diff.hpp"
#include "source_spellcheck.hpp" #include "source_spellcheck.hpp"
#include "tooltips.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: public:
static std::set<View *> non_deleted_views; static std::set<View *> non_deleted_views;
static std::set<View *> views; static std::set<View *> views;

157
src/source_coverage.cpp

@ -0,0 +1,157 @@
#include "source_coverage.hpp"
#include "info.hpp"
#include <algorithm>
#include <sstream>
Source::CoverageView::Renderer::Renderer() : Gsv::GutterRendererText() {
set_padding(4, 0);
}
Source::CoverageView::CoverageView(const boost::filesystem::path &file_path, const Glib::RefPtr<Gsv::Language> &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<Coverage::LineCoverage>{};
}
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());
}
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<Gtk::Tooltip> &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::LineCoverage> &&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<unsigned long>(line);
});
if(it != line_coverage.end() && it->line == static_cast<unsigned long>(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(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();
}

34
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<Gsv::Language> &language);
~CoverageView() override;
void configure() override;
std::function<std::vector<Coverage::LineCoverage>()> get_coverage;
void toggle_coverage();
private:
std::unique_ptr<Renderer> renderer;
std::vector<Coverage::LineCoverage> line_coverage;
void update_coverage(std::vector<Coverage::LineCoverage> &&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

7
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", []() { menu.add_action("project_set_run_arguments", []() {
auto project = Project::create(); auto project = Project::create();
auto run_arguments = project->get_run_arguments(); 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_implement_method"]->set_enabled(view && view->get_method);
menu.actions["source_goto_next_diagnostic"]->set_enabled(view && view->goto_next_diagnostic); 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_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 #ifdef JUCI_ENABLE_DEBUG
Project::debug_activate_menu_items(); Project::debug_activate_menu_items();
#endif #endif

Loading…
Cancel
Save