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