blob: 6ae0fbbfa45267952352ca04faa2cc85eeeac0af [file] [edit]
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmListFileCoverage.h"
#include <algorithm>
#include <array>
#include <cstdio>
#include <memory>
#include <utility>
#include <cm/optional>
#include <cm/string_view>
#include <cmext/algorithm>
#include <cmext/string_view>
#include <cm3p/uv.h>
#include "cmsys/FStream.hxx"
#include "cmGeneratedFileStream.h"
#include "cmListFileCache.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
namespace {
std::unique_ptr<cmListFileCoverage> Instance;
bool IsExecutable(cmListFileFunction const& lff)
{
if (lff.Line() <= 0) {
return false;
}
static constexpr std::array<cm::string_view, 7> notExecutable{
{ "else"_s, "endif"_s, "endforeach"_s, "endwhile"_s, "endfunction"_s,
"endmacro"_s, "endblock"_s }
};
return !cm::contains(notExecutable, lff.LowerCaseName());
}
}
cmListFileCoverage::cmListFileCoverage(std::string outputPath)
: OutputPath(std::move(outputPath))
{
}
bool cmListFileCoverage::Start(std::string& error)
{
if (Instance) {
return true;
}
cm::optional<std::string> const value =
cmSystemTools::GetEnvVar("CMAKE_LISTFILE_COVERAGE_DIR");
if (!value || value->empty()) {
return true;
}
std::string const& dir = *value;
if (!cmSystemTools::FileIsFullPath(dir)) {
error = cmStrCat("CMAKE_LISTFILE_COVERAGE_DIR is not an absolute path: \"",
dir, '"');
return false;
}
cmsys::Status const status = cmSystemTools::MakeDirectory(dir);
if (!status) {
error = cmStrCat("Cannot write CMake listfile coverage to directory \"",
dir, "\": ", status.GetString());
return false;
}
std::array<char, 9> token{};
std::snprintf(token.data(), token.size(), "%08x",
static_cast<unsigned int>(cmSystemTools::RandomNumber()));
std::string base = dir;
cmSystemTools::ConvertToUnixSlashes(base);
if (base.back() != '/') {
base += '/';
}
std::string outputPath = cmStrCat(
base, static_cast<long long>(uv_os_getpid()), '-', token.data(), ".info");
std::string const probePath = cmStrCat(outputPath, ".probe");
{
cmsys::ofstream probe(probePath.c_str());
if (!probe) {
error = cmStrCat("Cannot write CMake listfile coverage to directory \"",
dir, "\": ", cmSystemTools::GetLastSystemError());
return false;
}
}
cmSystemTools::RemoveFile(probePath);
Instance.reset(new cmListFileCoverage(std::move(outputPath)));
return true;
}
cmListFileCoverage* cmListFileCoverage::Get()
{
return Instance.get();
}
void cmListFileCoverage::Finish()
{
if (!Instance) {
return;
}
// A tracefile with no SF record is not valid, so write nothing then.
cmLcovTracefile const tracefile = Instance->ToTracefile();
if (tracefile.HasData()) {
cmGeneratedFileStream fout(Instance->OutputPath, true);
tracefile.Write(fout);
}
Instance.reset();
}
cmListFileCoverage::SyntheticCommandScope::SyntheticCommandScope(
cmListFileFunction const& lff)
{
cmListFileCoverage* cov = cmListFileCoverage::Get();
if (cov && cov->SyntheticCommands.insert(&lff).second) {
this->Command = &lff;
}
}
cmListFileCoverage::SyntheticCommandScope::~SyntheticCommandScope()
{
cmListFileCoverage* cov = cmListFileCoverage::Get();
if (cov && this->Command) {
cov->SyntheticCommands.erase(this->Command);
}
}
void cmListFileCoverage::FileParsed(
std::string const& path, std::vector<cmListFileFunction> const& functions)
{
cmLcovSourceFile& file =
this->Files[cmSystemTools::FileIsFullPath(path)
? path
: cmSystemTools::CollapseFullPath(path)];
for (long line : ExecutableLines(functions)) {
file.Lines.emplace(line, 0);
}
for (cmListFileFunction const& lff : functions) {
std::string const& name = lff.LowerCaseName();
if (lff.Line() > 0 && (name == "function"_s || name == "macro"_s) &&
!lff.Arguments().empty()) {
file.Functions.emplace(lff.Line(),
cmLcovFunction(lff.Arguments().front().Value));
}
}
}
void cmListFileCoverage::CommandRan(cmListFileFunction const& lff,
std::string const& path, long line)
{
if (this->SyntheticCommands.count(&lff)) {
return;
}
auto file = this->FindFile(path);
if (file == this->Files.end()) {
return;
}
auto hits = file->second.Lines.find(line);
if (hits != file->second.Lines.end()) {
++hits->second;
}
}
void cmListFileCoverage::FunctionCalled(std::string const& path,
long definitionLine)
{
auto file = this->FindFile(path);
if (file == this->Files.end()) {
return;
}
auto fn = file->second.Functions.find(definitionLine);
if (fn != file->second.Functions.end()) {
++fn->second.Hits;
}
}
std::map<std::string, cmLcovSourceFile>::iterator cmListFileCoverage::FindFile(
std::string const& path)
{
if (cmSystemTools::FileIsFullPath(path)) {
return this->Files.find(path);
}
return this->Files.find(cmSystemTools::CollapseFullPath(path));
}
cmLcovTracefile cmListFileCoverage::ToTracefile() const
{
cmLcovTracefile tracefile;
tracefile.Files = this->Files;
return tracefile;
}
std::vector<long> cmListFileCoverage::ExecutableLines(
std::vector<cmListFileFunction> const& functions)
{
std::vector<long> lines;
for (cmListFileFunction const& lff : functions) {
if (IsExecutable(lff)) {
lines.push_back(lff.Line());
}
}
std::sort(lines.begin(), lines.end());
lines.erase(std::unique(lines.begin(), lines.end()), lines.end());
return lines;
}