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