mirror of https://gitlab.com/cppit/jucipp
11 changed files with 467 additions and 3 deletions
@ -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 {}; |
||||
} |
||||
@ -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
|
||||
@ -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(); |
||||
} |
||||
@ -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
|
||||
Loading…
Reference in new issue