Files
CMake/Source/cmFunctionCommand.cxx
T
Mickaël Germain 37b38a29b6 cmake: Add CMAKE_LISTFILE_COVERAGE_DIR to record listfile coverage
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
2026-11-01 22:42:38 -08:00

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;
}