mirror of
https://github.com/Kitware/CMake.git
synced 2026-11-06 05:16:58 +00:00
Projects test their CMake code, but no tool tells them which parts of that code their tests run. Add a `CMAKE_LISTFILE_COVERAGE_DIR` environment variable. When it names a directory, each `cmake` process that runs CMake language code records which lines of each listfile ran and how many times each function and macro was called. At exit the process writes one lcov tracefile, `<pid>-<token>.info`, into that directory. An empty value means off. A relative path, or a directory that cannot be written, is an error at startup. Configure, script mode, `cmake --install`, `try_compile` projects, and a re-run of configure from the build system record coverage. `cmake --build`, `cmake -E`, `--help`, `--version`, and a `--check-build-system` step that does not re-run configure do not record, because they run no user code. The collector registers each file when CMake parses it, and counts a hit for each command that `cmMakefile::ExecuteCommand` runs. A command counts on the line where it starts. `else()` and the `end*()` commands do not count. `elseif()` counts each time its condition is evaluated, through a hook in `cmIfCommand`. Function and macro bodies count on their lines in the file that defines them, and each definition gets an lcov `FN` record with the number of calls. If one file defines the same name more than once, each record name gets an `@<line>` suffix. Commands that another command runs on the same line, such as those of `cmake_language(CALL)` and `list()` callbacks, add no hit. Code from `cmake_language(EVAL CODE)` is left out, because it has no file. Add `cmLcovTracefile` to read, merge, and write lcov tracefiles, so that later changes can merge per-process files in ctest and read them in `ctest_coverage()`. Issue: #28192
217 lines
6.9 KiB
C++
217 lines
6.9 KiB
C++
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
file LICENSE.rst or https://cmake.org/licensing for details. */
|
|
#include "cmFunctionCommand.h"
|
|
|
|
#include <functional>
|
|
#include <utility>
|
|
|
|
#include <cm/memory>
|
|
#include <cm/string_view>
|
|
#include <cmext/algorithm>
|
|
#include <cmext/string_view>
|
|
|
|
#include "cmDiagnostics.h"
|
|
#include "cmExecutionStatus.h"
|
|
#include "cmFunctionBlocker.h"
|
|
#include "cmList.h"
|
|
#include "cmListFileCache.h"
|
|
#include "cmMakefile.h"
|
|
#include "cmPolicies.h"
|
|
#include "cmRange.h"
|
|
#include "cmState.h"
|
|
#include "cmStateTypes.h"
|
|
#include "cmStringAlgorithms.h"
|
|
#include "cmSystemTools.h"
|
|
|
|
#ifndef CMAKE_BOOTSTRAP
|
|
# include "cmListFileCoverage.h"
|
|
#endif
|
|
|
|
namespace {
|
|
std::string const ARGC = "ARGC";
|
|
std::string const ARGNC = "ARGNC";
|
|
std::string const ARGN = "ARGN";
|
|
std::string const ARGV = "ARGV";
|
|
std::string const CMAKE_CURRENT_FUNCTION = "CMAKE_CURRENT_FUNCTION";
|
|
std::string const CMAKE_CURRENT_FUNCTION_LIST_FILE =
|
|
"CMAKE_CURRENT_FUNCTION_LIST_FILE";
|
|
std::string const CMAKE_CURRENT_FUNCTION_LIST_DIR =
|
|
"CMAKE_CURRENT_FUNCTION_LIST_DIR";
|
|
std::string const CMAKE_CURRENT_FUNCTION_LIST_LINE =
|
|
"CMAKE_CURRENT_FUNCTION_LIST_LINE";
|
|
|
|
// define the class for function commands
|
|
class cmFunctionHelperCommand
|
|
{
|
|
public:
|
|
/**
|
|
* This is called when the command is first encountered in
|
|
* the CMakeLists.txt file.
|
|
*/
|
|
bool operator()(std::vector<cmListFileArgument> const& args,
|
|
cmExecutionStatus& inStatus) const;
|
|
|
|
std::vector<std::string> Args;
|
|
std::vector<cmListFileFunction> Functions;
|
|
cmPolicies::PolicyMap Policies;
|
|
cmDiagnostics::DiagnosticMap Diagnostics;
|
|
std::string FilePath;
|
|
long Line;
|
|
};
|
|
|
|
bool cmFunctionHelperCommand::operator()(
|
|
std::vector<cmListFileArgument> const& args,
|
|
cmExecutionStatus& inStatus) const
|
|
{
|
|
cmMakefile& makefile = inStatus.GetMakefile();
|
|
|
|
// Expand the argument list to the function.
|
|
std::vector<std::string> expandedArgs;
|
|
makefile.ExpandArguments(args, expandedArgs);
|
|
|
|
// make sure the number of arguments passed is at least the number
|
|
// required by the signature
|
|
if (expandedArgs.size() < this->Args.size() - 1) {
|
|
auto const errorMsg = cmStrCat(
|
|
"Function invoked with incorrect arguments for function named: ",
|
|
this->Args.front());
|
|
inStatus.SetError(errorMsg);
|
|
return false;
|
|
}
|
|
|
|
#ifndef CMAKE_BOOTSTRAP
|
|
if (cmListFileCoverage* coverage = cmListFileCoverage::Get()) {
|
|
coverage->FunctionCalled(this->FilePath, this->Line);
|
|
}
|
|
#endif
|
|
|
|
cmMakefile::FunctionPushPop functionScope(&makefile, this->FilePath,
|
|
this->Policies, this->Diagnostics);
|
|
|
|
// set the value of argc
|
|
makefile.AddDefinition(ARGC, std::to_string(expandedArgs.size()));
|
|
makefile.MarkVariableAsUsed(ARGC);
|
|
|
|
// set the values for ARGV0 ARGV1 ...
|
|
for (auto t = 0u; t < expandedArgs.size(); ++t) {
|
|
auto const value = cmStrCat(ARGV, t);
|
|
makefile.AddDefinition(value, expandedArgs[t]);
|
|
makefile.MarkVariableAsUsed(value);
|
|
}
|
|
|
|
// define the formal arguments
|
|
for (auto j = 1u; j < this->Args.size(); ++j) {
|
|
makefile.AddDefinition(this->Args[j], expandedArgs[j - 1]);
|
|
}
|
|
|
|
// define ARGV, ARGN, and ARGNC
|
|
auto const argvDef = cmList::to_string(expandedArgs);
|
|
auto const expIt = expandedArgs.begin() + (this->Args.size() - 1);
|
|
auto const argnDef =
|
|
cmList::to_string(cmMakeRange(expIt, expandedArgs.end()));
|
|
auto const functionArgncDef =
|
|
std::to_string(expandedArgs.size() - (this->Args.size() - 1));
|
|
makefile.AddDefinition(ARGV, argvDef);
|
|
makefile.MarkVariableAsUsed(ARGV);
|
|
makefile.AddDefinition(ARGN, argnDef);
|
|
makefile.MarkVariableAsUsed(ARGN);
|
|
makefile.AddDefinition(ARGNC, functionArgncDef);
|
|
makefile.MarkVariableAsUsed(ARGNC);
|
|
|
|
makefile.AddDefinition(CMAKE_CURRENT_FUNCTION, this->Args.front());
|
|
makefile.MarkVariableAsUsed(CMAKE_CURRENT_FUNCTION);
|
|
makefile.AddDefinition(CMAKE_CURRENT_FUNCTION_LIST_FILE, this->FilePath);
|
|
makefile.MarkVariableAsUsed(CMAKE_CURRENT_FUNCTION_LIST_FILE);
|
|
makefile.AddDefinition(CMAKE_CURRENT_FUNCTION_LIST_DIR,
|
|
cmSystemTools::GetFilenamePath(this->FilePath));
|
|
makefile.MarkVariableAsUsed(CMAKE_CURRENT_FUNCTION_LIST_DIR);
|
|
makefile.AddDefinition(CMAKE_CURRENT_FUNCTION_LIST_LINE,
|
|
std::to_string(this->Line));
|
|
makefile.MarkVariableAsUsed(CMAKE_CURRENT_FUNCTION_LIST_LINE);
|
|
|
|
// Invoke all the functions that were collected in the block.
|
|
// for each function
|
|
for (cmListFileFunction const& func : this->Functions) {
|
|
cmExecutionStatus status(makefile);
|
|
if (!makefile.ExecuteCommand(func, status) || status.GetNestedError()) {
|
|
// The error message should have already included the call stack
|
|
// so we do not need to report an error here.
|
|
functionScope.Quiet();
|
|
inStatus.SetNestedError();
|
|
return false;
|
|
}
|
|
if (status.GetReturnInvoked()) {
|
|
makefile.RaiseScope(status.GetReturnVariables());
|
|
break;
|
|
}
|
|
if (status.HasExitCode()) {
|
|
inStatus.SetExitCode(status.GetExitCode());
|
|
break;
|
|
}
|
|
}
|
|
|
|
// pop scope on the makefile
|
|
return true;
|
|
}
|
|
|
|
class cmFunctionFunctionBlocker : public cmFunctionBlocker
|
|
{
|
|
public:
|
|
cm::string_view StartCommandName() const override { return "function"_s; }
|
|
cm::string_view EndCommandName() const override { return "endfunction"_s; }
|
|
|
|
bool ArgumentsMatch(cmListFileFunction const&,
|
|
cmMakefile& mf) const override;
|
|
|
|
bool Replay(std::vector<cmListFileFunction> functions,
|
|
cmExecutionStatus& status) override;
|
|
|
|
std::vector<std::string> Args;
|
|
};
|
|
|
|
bool cmFunctionFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff,
|
|
cmMakefile& mf) const
|
|
{
|
|
std::vector<std::string> expandedArguments;
|
|
mf.ExpandArguments(lff.Arguments(), expandedArguments);
|
|
return expandedArguments.empty() ||
|
|
expandedArguments.front() == this->Args.front();
|
|
}
|
|
|
|
bool cmFunctionFunctionBlocker::Replay(
|
|
std::vector<cmListFileFunction> functions, cmExecutionStatus& status)
|
|
{
|
|
cmMakefile& mf = status.GetMakefile();
|
|
// create a new command and add it to cmake
|
|
cmFunctionHelperCommand f;
|
|
f.Args = this->Args;
|
|
f.Functions = std::move(functions);
|
|
f.FilePath = this->GetStartingContext().FilePath;
|
|
f.Line = this->GetStartingContext().Line;
|
|
mf.RecordPolicies(f.Policies);
|
|
mf.RecordDiagnostics(f.Diagnostics);
|
|
return mf.GetState()->AddScriptedCommand(
|
|
this->Args.front(), cmStateEnums::CommandType::Function,
|
|
BT<cmState::Command>(std::move(f),
|
|
mf.GetBacktrace().Push(this->GetStartingContext())),
|
|
mf);
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
bool cmFunctionCommand(std::vector<std::string> const& args,
|
|
cmExecutionStatus& status)
|
|
{
|
|
if (args.empty()) {
|
|
status.SetError("called with incorrect number of arguments");
|
|
return false;
|
|
}
|
|
|
|
// create a function blocker
|
|
auto fb = cm::make_unique<cmFunctionFunctionBlocker>();
|
|
cm::append(fb->Args, args);
|
|
status.GetMakefile().AddFunctionBlocker(std::move(fb));
|
|
|
|
return true;
|
|
}
|