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
This commit is contained in:
Mickaël Germain committed 2026-11-01 22:42:38 -08:00
1 parent 173b0da32d
commit 37b38a29b6
86 files changed
+2100 -16

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
CMAKE_LISTFILE_COVERAGE_DIR
---------------------------
.. versionadded:: 4.6
.. include:: include/ENV_VAR.rst
Record coverage of CMake language code in lcov tracefiles.
When this environment variable is set to 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 this data into the directory. This variable alone turns coverage
on. There is no command-line option for it.
The value must be an absolute path. If the directory does not exist,
CMake creates it. If the variable is not set, or is set to an empty value,
coverage is off.
Child processes inherit the variable, so a :program:`cmake` process that a
project starts with :command:`execute_process` also records coverage. On
every platform, this form sets the variable for one command:
.. code-block:: console
$ cmake -E env CMAKE_LISTFILE_COVERAGE_DIR=/path/to/dir cmake -S src -B build
Modes that Record Coverage
^^^^^^^^^^^^^^^^^^^^^^^^^^
These modes record coverage:
* Configure and generate a project. This includes projects that
:command:`try_compile` and :command:`try_run` configure, which record into
the same file as the parent project.
* Script mode, :option:`cmake -P`.
* Install scripts, :option:`cmake --install`.
* A re-run of configure that the build system starts.
These modes do not record coverage and do not check the directory:
* :manual:`ccmake(1)` and :manual:`cmake-gui(1)`.
* :option:`cmake --build` and the :option:`cmake -E` commands. This
includes ``cmake -E cmake_depends``, which reads files that CMake
generates.
* :option:`cmake --help`, :option:`cmake --version`, :option:`ctest --help`
and :option:`ctest --version`.
* ``cmake --system-information``.
* ``cmake --check-build-system``, which the build system runs to check
whether it must re-run configure. If the check decides to re-run
configure, coverage starts at that point and the re-run records as usual.
The ``PreLoad.cmake`` file that CMake reads before the check is not
recorded in that run.
* The compile and link launchers that :module:`CTestUseLaunchers` enables,
which read a file that CMake generates, once for each compile.
Some other modes that run no CMake language code, such as ``cmake -N`` and
:option:`cmake --list-presets`, check the directory at startup, but write no
file.
Errors
^^^^^^
Before it runs any CMake language code, CMake makes sure that it can write
files in the directory. If the value is a relative path, or if CMake cannot
create the directory or write there, CMake reports an error and stops. A
failure to write the file at exit does not change the exit code.
What Gets Recorded
^^^^^^^^^^^^^^^^^^
Each file that CMake reads as a listfile gets a record, with the absolute
path that CMake used to read it. This includes files under
:variable:`CMAKE_ROOT` and files that CMake generates in the build tree.
Records can name generated files that CMake deletes later, such as the
projects that :command:`try_compile` configures, so report tools may need
to filter them out. Files that CMake never reads do not appear. A file
with no line that can count and no function or macro, such as a file with
only comments, gets no record, and a process that has no record writes no
file.
Lines:
* A command counts on the line where it starts, once each time it runs, even
if its arguments span several lines.
* Opening commands such as :command:`if`, :command:`foreach`,
:command:`while`, :command:`block`, :command:`function` and
:command:`macro` count like any other command. A :command:`while`
command counts once each time the loop starts, not once for each
evaluation of its condition.
* An :command:`elseif` counts each time CMake evaluates its condition.
* :command:`else` and the commands that close a block, such as
:command:`endif` and :command:`endfunction`, do not count. Comments and
blank lines do not count.
* A command in the body of a function or macro counts each time the body
runs, on its line in the file that defines the function or macro.
* A deferred call made with :command:`cmake_language(DEFER)` counts on its
own line when it runs.
* A command that CMake skips because of an earlier fatal error does not
count.
* A command that another command runs on the same line does not add a hit.
Examples are the command that :command:`cmake_language(CALL)` runs, and
the commands that :command:`list` operations call.
* Code that :command:`cmake_language(EVAL CODE) <cmake_language(EVAL)>` runs
is left out, because it has no file. The :command:`cmake_language` line
itself counts.
* Each line that can count appears in the record, with zero hits if it
never ran.
Functions and macros:
* Each :command:`function` and :command:`macro` definition gets one
function record, with the name as written in the source and the line of
the definition. Its count is the number of calls.
* If one file defines the same name more than once, each definition gets
its own record, named ``<name>@<line>``, for example ``foo@120`` and
``foo@135``. A name that one file defines once keeps its name.
* A name is percent-encoded as in URLs: ``%`` is written as ``%25``, a
comma as ``%2C``, a line break as ``%0A`` or ``%0D``, and ``@`` as
``%40``. Other characters are written as they are. For example, a
function named ``a,b`` is written as ``a%2Cb``.
* When CMake reads a tracefile back, it removes the ``@<line>`` suffix and
decodes the name, so every name reads back as written.
Output Files
^^^^^^^^^^^^
Each process that records coverage writes one lcov tracefile, named
``<pid>-<token>.info``, where ``<pid>`` is the process ID and ``<token>``
is a random value. The file uses the ``SF``, ``FN``, ``FNDA``, ``FNF``,
``FNH``, ``DA``, ``LF`` and ``LH`` records. CMake never deletes or merges
files in the directory. Clear the directory before a new run.
+1
View File
@@ -70,6 +70,7 @@ Environment Variables that Control the Build
/envvar/CMAKE_LANG_IMPLICIT_LINK_DIRECTORIES_EXCLUDE
/envvar/CMAKE_LANG_IMPLICIT_LINK_LIBRARIES_EXCLUDE
/envvar/CMAKE_LANG_LINKER_LAUNCHER
/envvar/CMAKE_LISTFILE_COVERAGE_DIR
/envvar/CMAKE_MSVCIDE_RUN_PATH
/envvar/CMAKE_NO_VERBOSE
/envvar/CMAKE_OSX_ARCHITECTURES
+5
View File
@@ -0,0 +1,5 @@
listfile-coverage
-----------------
* The :envvar:`CMAKE_LISTFILE_COVERAGE_DIR` environment variable was added
to record coverage of CMake language code in lcov tracefiles.
+4
View File
@@ -421,6 +421,10 @@ add_library(
cmList.cxx
cmListFileCache.cxx
cmListFileCache.h
cmLcovTracefile.cxx
cmLcovTracefile.h
cmListFileCoverage.cxx
cmListFileCoverage.h
cmLocalCommonGenerator.cxx
cmLocalCommonGenerator.h
cmLocalGenerator.cxx
+10
View File
@@ -46,6 +46,10 @@
#include "cmValue.h"
#include "cmake.h"
#ifndef CMAKE_BOOTSTRAP
# include "cmListFileCoverage.h"
#endif
namespace cm {
enum class TargetType;
}
@@ -135,6 +139,12 @@ bool cmCMakeLanguageCommandCALL(std::vector<cmListFileArgument> const& args,
}
return true;
}
#ifndef CMAKE_BOOTSTRAP
cm::optional<cmListFileCoverage::SyntheticCommandScope> syntheticScope;
if (cmListFileCoverage::Get()) {
syntheticScope.emplace(func);
}
#endif
return makefile.ExecuteCommand(func, status);
}
+10
View File
@@ -23,6 +23,10 @@
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#ifndef CMAKE_BOOTSTRAP
# include "cmListFileCoverage.h"
#endif
namespace {
std::string const ARGC = "ARGC";
std::string const ARGNC = "ARGNC";
@@ -75,6 +79,12 @@ bool cmFunctionHelperCommand::operator()(
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);
+10
View File
@@ -22,6 +22,10 @@
#include "cmSystemTools.h"
#include "cmake.h"
#ifndef CMAKE_BOOTSTRAP
# include "cmListFileCoverage.h"
#endif
static std::string cmIfCommandError(
std::vector<cmExpandedCommandArgument> const& args)
{
@@ -122,6 +126,12 @@ bool cmIfFunctionBlocker::Replay(std::vector<cmListFileFunction> functions,
mf.PrintCommandTrace(func, elseifBT,
cmMakefile::CommandMissingFromStack::Yes);
}
#ifndef CMAKE_BOOTSTRAP
if (cmListFileCoverage* coverage = cmListFileCoverage::Get()) {
coverage->CommandRan(func, this->GetStartingContext().FilePath,
func.Line());
}
#endif
std::string errorString;
+289
View File
@@ -0,0 +1,289 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmLcovTracefile.h"
#include <algorithm>
#include <array>
#include <cstdio>
#include <ostream>
#include <utility>
#include <vector>
#include <cm/string_view>
#include <cmext/algorithm>
#include <cmext/string_view>
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
cmLcovFunction::cmLcovFunction(std::string name, unsigned long long hits)
: Name(std::move(name))
, Hits(hits)
{
}
bool cmLcovSourceFile::HasData() const
{
return !this->Lines.empty() || !this->Functions.empty();
}
namespace {
void MergeSourceFile(cmLcovSourceFile& into, cmLcovSourceFile const& from)
{
for (auto const& line : from.Lines) {
into.Lines[line.first] += line.second;
}
for (auto const& fn : from.Functions) {
auto it = into.Functions.find(fn.first);
if (it == into.Functions.end()) {
into.Functions.emplace(fn.first, fn.second);
} else {
it->second.Hits += fn.second.Hits;
}
}
}
// Percent-encode, as in URLs, the characters that a name can not hold in
// lcov records: "," and line breaks. Also encode "%" and the "@" of the
// "@<line>" suffix, so that every name reads back as written.
std::string EncodeName(std::string const& name)
{
std::string out;
for (char c : name) {
switch (c) {
case '%':
case ',':
case '\n':
case '\r':
case '@': {
std::array<char, 4> hex{};
std::snprintf(hex.data(), hex.size(), "%%%02X",
static_cast<unsigned char>(c));
out += hex.data();
} break;
default:
out += c;
break;
}
}
return out;
}
// The exact inverse of EncodeName.
std::string DecodeName(std::string const& name)
{
return cmSystemTools::DecodeURL(name);
}
bool ParseLine(std::string const& s, long* line)
{
return cmStrToLong(s, line) && *line > 0;
}
// Split "<a>,<b>" at the first comma.
bool SplitPair(std::string const& value, std::string* first,
std::string* second)
{
std::string::size_type const comma = value.find(',');
if (comma == std::string::npos) {
return false;
}
*first = value.substr(0, comma);
*second = value.substr(comma + 1);
return true;
}
// If NAME ends with "@<line>" for the given definition line, strip that
// suffix. The writer adds it to keep duplicate names unique.
std::string StripLineSuffix(std::string const& name, long line)
{
std::string const suffix = cmStrCat('@', line);
if (name.size() > suffix.size() && cmHasSuffix(name, suffix)) {
return name.substr(0, name.size() - suffix.size());
}
return name;
}
}
void cmLcovTracefile::Merge(cmLcovTracefile const& other)
{
for (auto const& file : other.Files) {
MergeSourceFile(this->Files[file.first], file.second);
}
}
bool cmLcovTracefile::HasData() const
{
return std::any_of(
this->Files.begin(), this->Files.end(),
[](std::pair<std::string const, cmLcovSourceFile> const& file) {
return file.second.HasData();
});
}
void cmLcovTracefile::Write(std::ostream& os) const
{
if (!this->TestName.empty()) {
os << "TN:" << this->TestName << '\n';
}
for (auto const& file : this->Files) {
cmLcovSourceFile const& data = file.second;
// lcov rejects a tracefile that has only such records.
if (!data.HasData()) {
continue;
}
os << "SF:" << file.first << '\n';
// Give each record a name that is unique in this block.
std::map<std::string, unsigned int> nameCounts;
std::vector<std::pair<long, std::string>> names;
names.reserve(data.Functions.size());
for (auto const& fn : data.Functions) {
names.emplace_back(fn.first, EncodeName(fn.second.Name));
++nameCounts[names.back().second];
}
for (auto& name : names) {
if (nameCounts[name.second] > 1) {
name.second = cmStrCat(name.second, '@', name.first);
}
}
for (auto const& name : names) {
os << "FN:" << name.first << ',' << name.second << '\n';
}
std::size_t functionsHit = 0;
for (auto const& name : names) {
unsigned long long const hits = data.Functions.at(name.first).Hits;
os << "FNDA:" << hits << ',' << name.second << '\n';
if (hits > 0) {
++functionsHit;
}
}
os << "FNF:" << data.Functions.size() << '\n';
os << "FNH:" << functionsHit << '\n';
std::size_t linesHit = 0;
for (auto const& line : data.Lines) {
os << "DA:" << line.first << ',' << line.second << '\n';
if (line.second > 0) {
++linesHit;
}
}
os << "LF:" << data.Lines.size() << '\n';
os << "LH:" << linesHit << '\n';
os << "end_of_record\n";
}
}
bool cmLcovTracefile::Read(std::istream& is, std::string& error)
{
bool inBlock = false;
bool sawFile = false;
std::string path;
cmLcovSourceFile block;
// Written function name -> definition line, for FNDA records.
std::map<std::string, long> nameToLine;
std::string text;
unsigned long lineNumber = 0;
auto fail = [&](cm::string_view message) -> bool {
error = cmStrCat("line ", lineNumber, ": ", message);
return false;
};
while (cmSystemTools::GetLineFromStream(is, text)) {
++lineNumber;
if (!text.empty() && text.back() == '\r') {
text.pop_back();
}
if (text.empty()) {
continue;
}
std::string key;
std::string value;
std::string::size_type const colon = text.find(':');
if (colon == std::string::npos) {
key = text;
} else {
key = text.substr(0, colon);
value = text.substr(colon + 1);
}
if (key == "TN") {
this->TestName = value;
continue;
}
if (key == "SF") {
if (inBlock) {
return fail("SF record before end_of_record");
}
inBlock = true;
sawFile = true;
path = value;
continue;
}
static constexpr std::array<cm::string_view, 8> blockRecords{
{ "FN"_s, "FNDA"_s, "DA"_s, "FNF"_s, "FNH"_s, "LF"_s, "LH"_s,
"end_of_record"_s }
};
if (!cm::contains(blockRecords, key)) {
// Unknown records and the branch records BRDA, BRF and BRH.
continue;
}
if (!inBlock) {
return fail(cmStrCat(key, " record before SF"));
}
if (key == "FN") {
std::string lineText;
std::string name;
long line = 0;
if (!SplitPair(value, &lineText, &name) || !ParseLine(lineText, &line)) {
return fail(cmStrCat("malformed FN record \"", value, '"'));
}
nameToLine[name] = line;
block.Functions[line].Name = DecodeName(StripLineSuffix(name, line));
} else if (key == "FNDA") {
std::string hitsText;
std::string name;
unsigned long long hits = 0;
if (!SplitPair(value, &hitsText, &name) ||
!cmStrToULongLong(hitsText, &hits)) {
return fail(cmStrCat("malformed FNDA record \"", value, '"'));
}
auto it = nameToLine.find(name);
if (it == nameToLine.end()) {
return fail(cmStrCat("FNDA record for \"", name, "\" with no FN"));
}
block.Functions[it->second].Hits += hits;
} else if (key == "DA") {
// A third field, the checksum, is ignored.
std::string lineText;
std::string rest;
long line = 0;
unsigned long long hits = 0;
if (!SplitPair(value, &lineText, &rest) || !ParseLine(lineText, &line) ||
!cmStrToULongLong(rest.substr(0, rest.find(',')), &hits)) {
return fail(cmStrCat("malformed DA record \"", value, '"'));
}
block.Lines[line] += hits;
} else if (key == "end_of_record") {
MergeSourceFile(this->Files[path], block);
block = cmLcovSourceFile();
nameToLine.clear();
inBlock = false;
}
// FNF, FNH, LF and LH are computed again by Write.
}
if (inBlock) {
return fail("missing end_of_record");
}
if (!sawFile) {
return fail("no SF record");
}
return true;
}
+65
View File
@@ -0,0 +1,65 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <iosfwd>
#include <map>
#include <string>
/** \brief Hit count of one function in an lcov tracefile. */
struct cmLcovFunction
{
// A constructor, so that brace init such as { "f1", 2 } works in C++11.
cmLcovFunction(std::string name = std::string(),
unsigned long long hits = 0);
// The name as written, without any "@<line>" suffix.
std::string Name;
unsigned long long Hits;
};
/** \brief Line and function hit counts of one source file. */
struct cmLcovSourceFile
{
/** True if the file has a line or a function record. */
bool HasData() const;
// DA records: line -> hits.
std::map<long, unsigned long long> Lines;
// FN records: definition line -> data.
std::map<long, cmLcovFunction> Functions;
};
/** \class cmLcovTracefile
* \brief Read, merge and write lcov tracefiles.
*
* The model holds the line and function records of the lcov tracefile
* format. Branch records are not kept.
*/
class cmLcovTracefile
{
public:
// TN record, empty means none.
std::string TestName;
// SF path -> data.
std::map<std::string, cmLcovSourceFile> Files;
/** Add the counts of another tracefile. Lines and functions are united
and their hits added. A function keeps its existing name. The test
name does not change. */
void Merge(cmLcovTracefile const& other);
/** True if some file has a line or a function record. */
bool HasData() const;
/** Write the tracefile in lcov format. Leave out the files that have
no line and no function record. */
void Write(std::ostream& os) const;
/** Read a tracefile in lcov format and add its records to this object.
Callers read into a fresh object. On failure the object holds
partial data and the caller must discard it. */
bool Read(std::istream& is, std::string& error);
};
+22
View File
@@ -30,6 +30,10 @@
#include "cmSystemTools.h"
#include "cmValue.h"
#ifndef CMAKE_BOOTSTRAP
# include "cmListFileCoverage.h"
#endif
cm::string_view cmList::element_separator{ ";" };
cmList cmList::sublist(size_type pos, size_type length) const
@@ -137,6 +141,12 @@ public:
cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
std::move(funcArgs) };
#ifndef CMAKE_BOOTSTRAP
cm::optional<cmListFileCoverage::SyntheticCommandScope> syntheticScope;
if (cmListFileCoverage::Get()) {
syntheticScope.emplace(func);
}
#endif
cmExecutionStatus status(*this->Makefile);
if (!this->Makefile->ExecuteCommand(func, status) ||
status.GetNestedError()) {
@@ -208,6 +218,12 @@ public:
cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
std::move(funcArgs) };
#ifndef CMAKE_BOOTSTRAP
cm::optional<cmListFileCoverage::SyntheticCommandScope> syntheticScope;
if (cmListFileCoverage::Get()) {
syntheticScope.emplace(func);
}
#endif
cmExecutionStatus status(*this->Makefile);
if (!this->Makefile->ExecuteCommand(func, status) ||
status.GetNestedError()) {
@@ -825,6 +841,12 @@ protected:
cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
std::move(funcArgs) };
#ifndef CMAKE_BOOTSTRAP
cm::optional<cmListFileCoverage::SyntheticCommandScope> syntheticScope;
if (cmListFileCoverage::Get()) {
syntheticScope.emplace(func);
}
#endif
cmExecutionStatus status(*this->Makefile);
if (!this->Makefile->ExecuteCommand(func, status) ||
status.GetNestedError()) {
+197
View File
@@ -0,0 +1,197 @@
/* 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[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->Files.find(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->Files.find(path);
if (file == this->Files.end()) {
return;
}
auto fn = file->second.Functions.find(definitionLine);
if (fn != file->second.Functions.end()) {
++fn->second.Hits;
}
}
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;
}
+83
View File
@@ -0,0 +1,83 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <map>
#include <set>
#include <string>
#include <vector>
#include "cmLcovTracefile.h"
class cmListFileFunction;
/** \class cmListFileCoverage
* \brief Record line hits and function calls of CMake listfiles.
*
* The environment variable CMAKE_LISTFILE_COVERAGE_DIR turns the collector
* on. One collector exists per process. At exit it writes one lcov
* tracefile into that directory. CMake runs listfile code on one thread,
* so the collector does no locking.
*/
class cmListFileCoverage
{
public:
/** Read CMAKE_LISTFILE_COVERAGE_DIR. Unset or empty: coverage stays off,
returns true. Otherwise check the path, create the directory, and
test that a file can be written there. On failure return false and
set error to the message text, without the "CMake Error: " prefix.
A second call while coverage is on does nothing and returns true. */
static bool Start(std::string& error);
/** The collector, or nullptr while coverage is off. */
static cmListFileCoverage* Get();
/** Write the tracefile if any registered file has a line or a
function. Ignore write failures. Turn coverage off. */
static void Finish();
/** While alive, CommandRan(lff, ...) ignores this exact object. Used
where one command builds another on its own line. */
class SyntheticCommandScope
{
public:
explicit SyntheticCommandScope(cmListFileFunction const& lff);
~SyntheticCommandScope();
SyntheticCommandScope(SyntheticCommandScope const&) = delete;
SyntheticCommandScope& operator=(SyntheticCommandScope const&) = delete;
private:
cmListFileFunction const* Command = nullptr;
};
/** Register the executable lines and the function and macro definitions
of a parsed file. A second call for the same path adds new lines and
functions and keeps existing counts. */
void FileParsed(std::string const& path,
std::vector<cmListFileFunction> const& functions);
/** Count a hit on a line of a registered file. */
void CommandRan(cmListFileFunction const& lff, std::string const& path,
long line);
/** Count a call of the function or macro defined on a line of a
registered file. */
void FunctionCalled(std::string const& path, long definitionLine);
/** The recorded data, with no test name. */
cmLcovTracefile ToTracefile() const;
/** The sorted, unique start lines of the commands that count. */
static std::vector<long> ExecutableLines(
std::vector<cmListFileFunction> const& functions);
private:
explicit cmListFileCoverage(std::string outputPath);
std::string OutputPath;
std::map<std::string, cmLcovSourceFile> Files;
std::set<cmListFileFunction const*> SyntheticCommands;
};
+12
View File
@@ -23,6 +23,10 @@
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#ifndef CMAKE_BOOTSTRAP
# include "cmListFileCoverage.h"
#endif
namespace {
// define the class for macro commands
@@ -41,6 +45,7 @@ public:
cmPolicies::PolicyMap Policies;
cmDiagnostics::DiagnosticMap Diagnostics;
std::string FilePath;
long Line = 0;
};
bool cmMacroHelperCommand::operator()(
@@ -63,6 +68,12 @@ bool cmMacroHelperCommand::operator()(
return false;
}
#ifndef CMAKE_BOOTSTRAP
if (cmListFileCoverage* coverage = cmListFileCoverage::Get()) {
coverage->FunctionCalled(this->FilePath, this->Line);
}
#endif
// set the value of argc
std::string argcDef = std::to_string(expandedArgs.size());
@@ -201,6 +212,7 @@ bool cmMacroFunctionBlocker::Replay(std::vector<cmListFileFunction> functions,
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(
+24
View File
@@ -70,6 +70,7 @@
#include "cmake.h"
#ifndef CMAKE_BOOTSTRAP
# include "cmListFileCoverage.h"
# include "cmMakefileProfilingData.h"
# include "cmVariableWatch.h"
#endif
@@ -626,6 +627,11 @@ bool cmMakefile::ExecuteCommand(cmListFileFunction const& lff,
if (this->GetCMakeInstance()->GetTrace()) {
this->PrintCommandTrace(lff, this->Backtrace);
}
#ifndef CMAKE_BOOTSTRAP
if (cmListFileCoverage* coverage = cmListFileCoverage::Get()) {
coverage->CommandRan(lff, this->Backtrace.Top().FilePath, lff.Line());
}
#endif
// Try invoking the command.
bool invokeSucceeded = command(lff.Arguments(), status);
bool hadNestedError = status.GetNestedError();
@@ -773,6 +779,12 @@ bool cmMakefile::ReadDependentFile(std::string const& filename,
}
#endif
#ifndef CMAKE_BOOTSTRAP
if (cmListFileCoverage* coverage = cmListFileCoverage::Get()) {
coverage->FileParsed(filenametoread, listFile.Functions);
}
#endif
this->RunListFile(listFile, filenametoread);
if (cmSystemTools::GetFatalErrorOccurred()) {
incScope.Quiet();
@@ -895,6 +907,12 @@ bool cmMakefile::ReadListFile(std::string const& filename)
}
#endif
#ifndef CMAKE_BOOTSTRAP
if (cmListFileCoverage* coverage = cmListFileCoverage::Get()) {
coverage->FileParsed(filenametoread, listFile.Functions);
}
#endif
this->RunListFile(listFile, filenametoread);
if (cmSystemTools::GetFatalErrorOccurred()) {
scope.Quiet();
@@ -1712,6 +1730,12 @@ void cmMakefile::Configure()
}
#endif
#ifndef CMAKE_BOOTSTRAP
if (cmListFileCoverage* coverage = cmListFileCoverage::Get()) {
coverage->FileParsed(currentStart, listFile.Functions);
}
#endif
if (this->IsRootMakefile()) {
bool hasVersion = false;
// search for the right policy command
+30 -16
View File
@@ -90,6 +90,7 @@
# include "cmInstrumentation.h"
# include "cmInstrumentationInterrupt.h"
# include "cmInstrumentationQuery.h"
# include "cmListFileCoverage.h"
# include "cmMakefileProfilingData.h"
# include "cmVariableWatch.h"
#endif
@@ -3262,6 +3263,25 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
cmSystemTools::PutEnv("MAKEFLAGS=");
}
// After generating fbuild.bff, FastBuild sees rebuild-bff as outdated since
// it hasn’t built the target yet. To make it a no-op for future runs, we
// trigger a dummy fbuild invocation that creates this marker file and runs
// CMake, marking rebuild-bff as up-to-date. That run only touches a file,
// so check for it before PreLoad.cmake, and run no CMake code.
std::string const FBuildRestatFile =
cmStrCat(this->GetHomeOutputDirectory(), '/', FASTBUILD_RESTAT_FILE);
if (!noconfigure && cmSystemTools::FileExists(FBuildRestatFile)) {
cmsys::ifstream restat(FBuildRestatFile.c_str(),
std::ios::in | std::ios::binary);
std::string const file((std::istreambuf_iterator<char>(restat)),
std::istreambuf_iterator<char>());
// On Windows can not delete file if it's still opened.
restat.close();
cmSystemTools::Touch(file, true);
cmSystemTools::RemoveFile(FBuildRestatFile);
return 0;
}
this->PreLoadCMakeFiles();
if (noconfigure) {
@@ -3273,23 +3293,17 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
if (!this->CheckBuildSystem()) {
return 0;
}
// After generating fbuild.bff, FastBuild sees rebuild-bff as outdated since
// it hasn’t built the target yet. To make it a no-op for future runs, we
// trigger a dummy fbuild invocation that creates this marker file and runs
// CMake, marking rebuild-bff as up-to-date.
std::string const FBuildRestatFile =
cmStrCat(this->GetHomeOutputDirectory(), '/', FASTBUILD_RESTAT_FILE);
if (cmSystemTools::FileExists(FBuildRestatFile)) {
cmsys::ifstream restat(FBuildRestatFile.c_str(),
std::ios::in | std::ios::binary);
std::string const file((std::istreambuf_iterator<char>(restat)),
std::istreambuf_iterator<char>());
// On Windows can not delete file if it's still opened.
restat.close();
cmSystemTools::Touch(file, true);
cmSystemTools::RemoveFile(FBuildRestatFile);
return 0;
#if !defined(CMAKE_BOOTSTRAP)
// A --check-build-system run records coverage only if it re-runs.
if (!this->CheckBuildSystemArgument.empty()) {
std::string error;
if (!cmListFileCoverage::Start(error)) {
cmSystemTools::Error(error);
return -1;
}
}
#endif
#ifdef CMake_ENABLE_DEBUGGER
if (!this->StartDebuggerIfEnabled()) {
+32
View File
@@ -49,6 +49,7 @@
# include "cmCMakePresetsArgs.h"
# include "cmCMakeSarifLogger.h"
# include "cmDocumentation.h"
# include "cmListFileCoverage.h"
#endif
#include "cmsys/Encoding.hxx"
@@ -275,6 +276,7 @@ int do_cmake(int ac, char const* const* av)
bool view_only = false;
#ifndef CMAKE_BOOTSTRAP
cm::optional<std::string> sarif_output_path;
bool check_build_system = false;
#endif
cmState::Role role = cmState::Role::Project;
std::vector<std::string> parsedArgs;
@@ -376,6 +378,12 @@ int do_cmake(int ac, char const* const* av)
}
if (!matched) {
parsedArgs.emplace_back(av[i]);
#ifndef CMAKE_BOOTSTRAP
if (arg == "--check-build-system") {
// cmake::Run starts coverage only if the build system re-runs.
check_build_system = true;
}
#endif
}
}
@@ -397,6 +405,16 @@ int do_cmake(int ac, char const* const* av)
cmakemainProgressCallback(msg, prog, &cm);
});
#ifndef CMAKE_BOOTSTRAP
if (!check_build_system) {
std::string error;
if (!cmListFileCoverage::Start(error)) {
cmSystemTools::Error(error);
return 1;
}
}
#endif
int res = cm.Run(parsedArgs, view_only);
if (list_cached || list_all_cached) {
std::cout << "-- Cache values" << std::endl;
@@ -1268,7 +1286,18 @@ int main(int ac, char const* const* av)
return do_build(ac, av);
}
if (strcmp(av[1], "--install") == 0) {
#ifndef CMAKE_BOOTSTRAP
std::string error;
if (!cmListFileCoverage::Start(error)) {
cmSystemTools::Error(error);
return 1;
}
int const ret = do_install(ac, av);
cmListFileCoverage::Finish();
return ret;
#else
return do_install(ac, av);
#endif
}
if (strcmp(av[1], "--open") == 0) {
return do_open(ac, av);
@@ -1278,6 +1307,9 @@ int main(int ac, char const* const* av)
}
}
int ret = do_cmake(ac, av);
#ifndef CMAKE_BOOTSTRAP
cmListFileCoverage::Finish();
#endif
if (uv_loop_t* loop = uv_default_loop()) {
uv_loop_close(loop);
}
+2
View File
@@ -20,6 +20,8 @@ set(CMakeLib_TESTS
testGenExListPredicate.cxx
testGenExListSortComparator.cxx
testJSONHelpers.cxx
testLcovTracefile.cxx
testListFileCoverage.cxx
testRST.cxx
testRange.cxx
testOptional.cxx
+244
View File
@@ -0,0 +1,244 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include <array>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include "cmLcovTracefile.h"
#include "testCommon.h"
namespace {
cmLcovTracefile makeSample()
{
cmLcovTracefile t;
t.TestName = "t1";
cmLcovSourceFile& f = t.Files["/s/a.cmake"];
f.Lines = { { 4, 1 }, { 2, 0 } };
f.Functions[11] = { "dup", 1 };
f.Functions[1] = { "f1", 2 };
f.Functions[14] = { "dup", 0 };
return t;
}
std::string writeText(cmLcovTracefile const& t)
{
std::ostringstream os;
t.Write(os);
return os.str();
}
bool readText(std::string const& text, cmLcovTracefile& t,
std::string* error = nullptr)
{
std::string localError;
std::istringstream is(text);
bool const ok = t.Read(is, error ? *error : localError);
if (!ok && !error) {
std::cout << "Read failed: " << localError << '\n';
}
return ok;
}
bool testWriteOrderAndDuplicateNames()
{
ASSERT_EQUAL(writeText(makeSample()),
"TN:t1\nSF:/s/a.cmake\n"
"FN:1,f1\nFN:11,dup@11\nFN:14,dup@14\n"
"FNDA:2,f1\nFNDA:1,dup@11\nFNDA:0,dup@14\n"
"FNF:3\nFNH:2\nDA:2,0\nDA:4,1\nLF:2\nLH:1\nend_of_record\n");
return true;
}
bool testReadRoundTrip()
{
std::string const text = writeText(makeSample());
cmLcovTracefile t;
ASSERT_TRUE(readText(text, t));
ASSERT_EQUAL(t.TestName, "t1");
ASSERT_EQUAL(t.Files["/s/a.cmake"].Functions[11].Name, "dup");
ASSERT_EQUAL(writeText(t), text);
return true;
}
bool testReadIgnoresUnknown()
{
cmLcovTracefile t;
ASSERT_TRUE(readText("VER:1\nSF:/x\nBRDA:1,0,0,1\nBRF:1\nBRH:1\nDA:1,3,abc\n"
"end_of_record\n",
t));
ASSERT_EQUAL(writeText(t),
"SF:/x\nFNF:0\nFNH:0\nDA:1,3\nLF:1\nLH:1\nend_of_record\n");
return true;
}
bool testReadCrlfAndDrive()
{
cmLcovTracefile t;
ASSERT_TRUE(readText("SF:C:/a b/x.cmake\r\nDA:1,1\r\nend_of_record\r\n", t));
ASSERT_EQUAL(t.Files.size(), 1u);
ASSERT_EQUAL(writeText(t),
"SF:C:/a b/x.cmake\nFNF:0\nFNH:0\nDA:1,1\nLF:1\nLH:1\n"
"end_of_record\n");
return true;
}
bool testReadSameFileTwice()
{
cmLcovTracefile t;
ASSERT_TRUE(readText("SF:/x\nDA:1,1\nend_of_record\n"
"SF:/x\nDA:1,2\nend_of_record\n",
t));
ASSERT_EQUAL(writeText(t),
"SF:/x\nFNF:0\nFNH:0\nDA:1,3\nLF:1\nLH:1\nend_of_record\n");
return true;
}
bool testReadStripsSuffix()
{
cmLcovTracefile t;
ASSERT_TRUE(
readText("SF:/x\nFN:12,foo@12\nFNDA:1,foo@12\nend_of_record\n", t));
std::map<long, cmLcovFunction> const& fns = t.Files["/x"].Functions;
ASSERT_EQUAL(fns.size(), 1u);
ASSERT_EQUAL(fns.begin()->first, 12);
ASSERT_EQUAL(fns.begin()->second.Name, "foo");
ASSERT_EQUAL(fns.begin()->second.Hits, 1u);
return true;
}
bool testReadErrors()
{
std::array<char const*, 5> const inputs{ {
"",
"DA:1,1\n",
"SF:/x\nDA:a,1\nend_of_record\n",
"SF:/x\nFNDA:1,f\nFN:1,f\nend_of_record\n",
"SF:/x\nDA:1,1\n",
} };
for (char const* input : inputs) {
cmLcovTracefile t;
std::string error;
if (readText(input, t, &error)) {
std::cout << "Read unexpectedly succeeded for '" << input << "'\n";
return false;
}
if (error.empty()) {
std::cout << "Read set no error for '" << input << "'\n";
return false;
}
}
return true;
}
bool testNameEncoding()
{
// Names are percent-encoded as in URLs, so that every name reads back.
cmLcovTracefile t;
std::map<long, cmLcovFunction>& fns = t.Files["/x"].Functions;
fns[3] = { "a,b", 0 };
fns[5] = { "50%", 0 };
fns[7] = { "x@y", 0 };
fns[9] = { "dup", 0 };
fns[11] = { "dup", 0 };
fns[13] = { "foo@13", 0 };
fns[15] = { "two\nlines", 0 };
fns[17] = { "d\re", 0 };
fns[19] = { "__check_flag", 0 };
std::string const text = writeText(t);
ASSERT_EQUAL(text,
"SF:/x\n"
"FN:3,a%2Cb\nFN:5,50%25\nFN:7,x%40y\nFN:9,dup@9\n"
"FN:11,dup@11\nFN:13,foo%4013\nFN:15,two%0Alines\n"
"FN:17,d%0De\nFN:19,__check_flag\n"
"FNDA:0,a%2Cb\nFNDA:0,50%25\nFNDA:0,x%40y\nFNDA:0,dup@9\n"
"FNDA:0,dup@11\nFNDA:0,foo%4013\nFNDA:0,two%0Alines\n"
"FNDA:0,d%0De\nFNDA:0,__check_flag\n"
"FNF:9\nFNH:0\nLF:0\nLH:0\nend_of_record\n");
cmLcovTracefile back;
ASSERT_TRUE(readText(text, back));
std::map<long, cmLcovFunction> const& backFns = back.Files["/x"].Functions;
ASSERT_EQUAL(backFns.size(), fns.size());
for (auto const& fn : fns) {
auto it = backFns.find(fn.first);
ASSERT_TRUE(it != backFns.end());
ASSERT_EQUAL(it->second.Name, fn.second.Name);
}
// Lowercase hexadecimal digits decode too. A "%" that does not start an
// escape stays as written.
cmLcovTracefile other;
ASSERT_TRUE(readText("SF:/x\nFN:1,a%2cb\nFN:2,p%zz\nFN:3,q%\n"
"end_of_record\n",
other));
std::map<long, cmLcovFunction> const& otherFns = other.Files["/x"].Functions;
ASSERT_EQUAL(otherFns.at(1).Name, "a,b");
ASSERT_EQUAL(otherFns.at(2).Name, "p%zz");
ASSERT_EQUAL(otherFns.at(3).Name, "q%");
return true;
}
bool testWriteSkipsEmptyFiles()
{
cmLcovTracefile t;
t.Files["/empty"];
t.Files["/lines"].Lines[1] = 0;
t.Files["/functions"].Functions[1] = { "f", 0 };
ASSERT_EQUAL(writeText(t),
"SF:/functions\nFN:1,f\nFNDA:0,f\nFNF:1\nFNH:0\nLF:0\nLH:0\n"
"end_of_record\n"
"SF:/lines\nFNF:0\nFNH:0\nDA:1,0\nLF:1\nLH:0\nend_of_record\n");
return true;
}
bool testHasData()
{
cmLcovTracefile t;
ASSERT_TRUE(!t.HasData());
t.Files["/empty"];
ASSERT_TRUE(!t.HasData());
t.Files["/lines"].Lines[1] = 0;
ASSERT_TRUE(t.HasData());
t.Files.erase("/lines");
t.Files["/functions"].Functions[1] = { "f", 0 };
ASSERT_TRUE(t.HasData());
return true;
}
bool testMerge()
{
cmLcovTracefile a;
a.TestName = "a";
a.Files["/x"].Lines = { { 1, 1 }, { 2, 0 } };
a.Files["/x"].Functions[1] = { "f", 1 };
cmLcovTracefile b;
b.TestName = "b";
b.Files["/x"].Lines = { { 2, 3 }, { 5, 1 } };
b.Files["/x"].Functions[1] = { "g", 2 };
b.Files["/y"].Lines = { { 1, 1 } };
a.Merge(b);
cmLcovTracefile expected;
expected.TestName = "a";
expected.Files["/x"].Lines = { { 1, 1 }, { 2, 3 }, { 5, 1 } };
expected.Files["/x"].Functions[1] = { "f", 3 };
expected.Files["/y"].Lines = { { 1, 1 } };
ASSERT_EQUAL(writeText(a), writeText(expected));
return true;
}
}
int testLcovTracefile(int /*unused*/, char* /*unused*/[])
{
return runTests({ testWriteOrderAndDuplicateNames, testReadRoundTrip,
testReadIgnoresUnknown, testReadCrlfAndDrive,
testReadSameFileTwice, testReadStripsSuffix,
testReadErrors, testNameEncoding, testWriteSkipsEmptyFiles,
testHasData, testMerge },
false);
}
+250
View File
@@ -0,0 +1,250 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include <sstream>
#include <string>
#include <vector>
#include "cmsys/Directory.hxx"
#include "cmLcovTracefile.h"
#include "cmListFileCache.h"
#include "cmListFileCoverage.h"
#include "cmSystemTools.h"
#include "testCommon.h"
#include "testConfig.h"
namespace {
std::vector<cmListFileFunction> parse(std::string const& text)
{
cmListFile lf;
if (!lf.ParseString(text, "test.cmake", nullptr, cmListFileBacktrace())) {
std::cout << "ParseString failed\n";
}
return lf.Functions;
}
std::string writeText(cmLcovTracefile const& t)
{
std::ostringstream os;
t.Write(os);
return os.str();
}
bool contains(std::string const& text, std::string const& part)
{
if (text.find(part) == std::string::npos) {
std::cout << "Missing '" << part << "' in:\n" << text;
return false;
}
return true;
}
// Turns coverage off on scope exit, so a failed ASSERT never leaves it on
// for the next test.
struct FinishGuard
{
FinishGuard() = default;
FinishGuard(FinishGuard const&) = delete;
FinishGuard& operator=(FinishGuard const&) = delete;
~FinishGuard() { cmListFileCoverage::Finish(); }
};
// A fresh directory, removed on scope exit. Declare it before the
// FinishGuard, so that the tracefile is written before the removal.
struct TempDir
{
TempDir()
: Path(BUILD_DIR "/testListFileCoverage-XXXXXX")
{
this->Ok = static_cast<bool>(cmSystemTools::MakeTempDirectory(this->Path));
if (!this->Ok) {
std::cout << "Cannot create temporary directory " << this->Path << '\n';
}
}
TempDir(TempDir const&) = delete;
TempDir& operator=(TempDir const&) = delete;
~TempDir()
{
if (this->Ok) {
cmSystemTools::RemoveADirectory(this->Path);
}
}
std::string Path;
bool Ok = false;
};
bool startCoverageIn(std::string const& dir)
{
cmSystemTools::PutEnv("CMAKE_LISTFILE_COVERAGE_DIR=" + dir);
std::string error;
bool const ok = cmListFileCoverage::Start(error);
cmSystemTools::UnPutEnv("CMAKE_LISTFILE_COVERAGE_DIR");
if (!ok) {
std::cout << "Start failed: " << error << '\n';
}
return ok;
}
unsigned long countEntries(std::string const& dir)
{
cmsys::Directory d;
if (!d.Load(dir)) {
return 0;
}
unsigned long n = 0;
for (unsigned long i = 0; i < d.GetNumberOfFiles(); ++i) {
std::string const name = d.GetFileName(i);
if (name != "." && name != "..") {
++n;
}
}
return n;
}
bool testExecutableLines()
{
std::vector<long> const lines =
cmListFileCoverage::ExecutableLines(parse("set(a 1)\n"
"if(a)\n"
"else()\n"
"endif()\n"
"function(f)\n"
"endfunction()\n"
"set(b\n"
" 2)\n"));
std::vector<long> const expected = { 1, 2, 5, 7 };
ASSERT_TRUE(lines == expected);
return true;
}
bool testRecording()
{
TempDir dir;
ASSERT_TRUE(dir.Ok);
FinishGuard guard;
ASSERT_TRUE(startCoverageIn(dir.Path));
cmListFileCoverage* cov = cmListFileCoverage::Get();
ASSERT_TRUE(cov != nullptr);
std::vector<cmListFileFunction> const functions = parse("function(f)\n"
" set(x 1)\n"
"endfunction()\n"
"f()\n");
cov->FileParsed("/s/a.cmake", functions);
cov->CommandRan(functions[0], "/s/a.cmake", 1);
cov->CommandRan(functions[3], "/s/a.cmake", 4);
cov->CommandRan(functions[1], "/s/a.cmake", 2);
cov->FunctionCalled("/s/a.cmake", 1);
// Ignored: the file is not registered.
cov->CommandRan(functions[0], "/s/other.cmake", 1);
// Ignored: endfunction is not an executable line.
cov->CommandRan(functions[2], "/s/a.cmake", 3);
// Ignored: no function is defined on this line.
cov->FunctionCalled("/s/a.cmake", 99);
std::string const text = writeText(cov->ToTracefile());
ASSERT_TRUE(contains(text, "SF:/s/a.cmake\n"));
ASSERT_TRUE(contains(text, "FN:1,f\n"));
ASSERT_TRUE(contains(text, "FNDA:1,f\n"));
ASSERT_TRUE(contains(text, "DA:1,1\nDA:2,1\nDA:4,1\nLF:3\nLH:3\n"));
ASSERT_TRUE(text.find("other.cmake") == std::string::npos);
ASSERT_TRUE(text.find("DA:3,") == std::string::npos);
ASSERT_TRUE(text.find("FN:99") == std::string::npos);
return true;
}
bool testFileParsedTwice()
{
TempDir dir;
ASSERT_TRUE(dir.Ok);
FinishGuard guard;
ASSERT_TRUE(startCoverageIn(dir.Path));
cmListFileCoverage* cov = cmListFileCoverage::Get();
ASSERT_TRUE(cov != nullptr);
std::vector<cmListFileFunction> const first = parse("set(a 1)\n");
cov->FileParsed("/s/a.cmake", first);
cov->CommandRan(first[0], "/s/a.cmake", 1);
cov->FileParsed("/s/a.cmake", parse("set(a 1)\nset(b 2)\n"));
std::string const text = writeText(cov->ToTracefile());
ASSERT_TRUE(contains(text, "DA:1,1\nDA:2,0\nLF:2\nLH:1\n"));
return true;
}
bool testSyntheticCommand()
{
TempDir dir;
ASSERT_TRUE(dir.Ok);
FinishGuard guard;
ASSERT_TRUE(startCoverageIn(dir.Path));
cmListFileCoverage* cov = cmListFileCoverage::Get();
ASSERT_TRUE(cov != nullptr);
cov->FileParsed("/s/a.cmake", parse("set(a 1)\n"));
cmListFileFunction const synthetic("set", 1, 1, {});
cmListFileFunction const real("set", 1, 1, {});
{
cmListFileCoverage::SyntheticCommandScope scope(synthetic);
cov->CommandRan(synthetic, "/s/a.cmake", 1);
cov->CommandRan(real, "/s/a.cmake", 1);
}
// The scope has ended, so the object counts again.
cov->CommandRan(synthetic, "/s/a.cmake", 1);
std::string const text = writeText(cov->ToTracefile());
ASSERT_TRUE(contains(text, "DA:1,2\n"));
return true;
}
bool testStartOffWhenUnset()
{
FinishGuard guard;
cmSystemTools::UnPutEnv("CMAKE_LISTFILE_COVERAGE_DIR");
std::string error;
ASSERT_TRUE(cmListFileCoverage::Start(error));
ASSERT_TRUE(cmListFileCoverage::Get() == nullptr);
return true;
}
bool testFinishWithNothingRegistered()
{
TempDir dir;
ASSERT_TRUE(dir.Ok);
FinishGuard guard;
ASSERT_TRUE(startCoverageIn(dir.Path));
cmListFileCoverage::Finish();
ASSERT_TRUE(cmListFileCoverage::Get() == nullptr);
ASSERT_EQUAL(countEntries(dir.Path), 0);
return true;
}
bool testFinishWithNoExecutableLines()
{
TempDir dir;
ASSERT_TRUE(dir.Ok);
{
FinishGuard guard;
ASSERT_TRUE(startCoverageIn(dir.Path));
cmListFileCoverage* cov = cmListFileCoverage::Get();
ASSERT_TRUE(cov != nullptr);
cov->FileParsed("/s/comments.cmake", parse("# Only a comment.\n"));
}
ASSERT_EQUAL(countEntries(dir.Path), 0);
return true;
}
}
int testListFileCoverage(int /*unused*/, char* /*unused*/[])
{
return runTests({ testExecutableLines, testRecording, testFileParsedTwice,
testSyntheticCommand, testStartOffWhenUnset,
testFinishWithNothingRegistered,
testFinishWithNoExecutableLines },
false);
}
+1
View File
@@ -655,6 +655,7 @@ endif()
add_RunCMake_test(LinkItemValidation)
add_RunCMake_test(LinkStatic)
add_RunCMake_test(ListFileCoverage)
add_RunCMake_test(ARCHIVER-prefix -DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID})
if(CMAKE_CXX_COMPILER_ID MATCHES "^(Cray|PGI|NVHPC|XL|XLClang|IBMClang|Fujitsu|FujitsuClang)$")
add_RunCMake_test(MetaCompileFeatures)
@@ -0,0 +1 @@
1
@@ -0,0 +1 @@
^CMake Error: Cannot write CMake listfile coverage to directory "[^"]*/BadDir-file/sub":
@@ -0,0 +1 @@
message(FATAL_ERROR "must not run")
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.31)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)
@@ -0,0 +1,10 @@
set(expect_sf_regex "/Callbacks\\.cmake$")
set(expect_lcov_regex
"\nDA:15,1\n"
"\nDA:16,1\n"
"\nDA:17,1\n"
"\nFNDA:3,keep\n"
"\nFNDA:[1-9][0-9]*,cmp\n"
"\nFNDA:3,twice\n"
)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,17 @@
function(keep value out)
set(${out} TRUE PARENT_SCOPE)
endfunction()
function(cmp a b out)
if(a STRLESS b)
set(${out} TRUE PARENT_SCOPE)
else()
set(${out} FALSE PARENT_SCOPE)
endif()
endfunction()
function(twice value out)
set(${out} "${value}${value}" PARENT_SCOPE)
endfunction()
set(l c a b)
list(FILTER l INCLUDE PREDICATE keep)
list(SORT l COMPARATOR cmp)
list(TRANSFORM l APPLY twice)
@@ -0,0 +1,3 @@
set(expect_sf_regex "CheckBuildSystem/CMakeLists\\.txt$")
set(forbid_sf_regex "check\\.cmake$" "PreLoad\\.cmake$")
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,3 @@
if(EXISTS "${cov_dir}")
set(RunCMake_TEST_FAILED "Coverage directory was created:\n ${cov_dir}")
endif()
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.31)
project(CheckBuildSystem NONE)
set(configured 1)
@@ -0,0 +1,3 @@
set(expect_files 2)
set(expect_lcov Child-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,14 @@
SF:<SRC>/Child.cmake
FNF:0
FNH:0
DA:1,1
LF:1
LH:1
end_of_record
SF:<SRC>/ChildScript.cmake
FNF:0
FNH:0
DA:1,1
LF:1
LH:1
end_of_record
@@ -0,0 +1 @@
execute_process(COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/ChildScript.cmake)
@@ -0,0 +1 @@
set(c 1)
@@ -0,0 +1,2 @@
set(expect_lcov CommentOnly-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,7 @@
SF:<SRC>/CommentOnly.cmake
FNF:0
FNH:0
DA:1,1
LF:1
LH:1
end_of_record
@@ -0,0 +1 @@
include(${CMAKE_CURRENT_LIST_DIR}/CommentOnlyLib.cmake)
@@ -0,0 +1 @@
# This file has only comments, so it gets no record.
@@ -0,0 +1,3 @@
# A process that reads only files with no record writes no file.
set(expect_files 0)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,13 @@
# A generator can run more cmake processes during the try_compile build.
set(expect_files "")
string(REGEX REPLACE "([][+.*?()^$|])" "\\\\\\1" modules_regex
"${CMAKE_ROOT}/Modules/")
set(expect_sf_regex
"/CMakeLists\\.txt$"
"/Configure\\.cmake$"
"/TryCompileProject/CMakeLists\\.txt$"
"^${modules_regex}"
)
# The DEFER command and the deferred set() both count on line 1.
set(expect_lcov_regex "(^|\n)SF:<SRC>/Configure\\.cmake\nFNF:0\nFNH:0\nDA:1,2\n")
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,2 @@
set(expect_sf_regex "/cmake_install\\.cmake$")
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,5 @@
cmake_language(DEFER CALL set deferred 1)
try_compile(tc PROJECT TryCompileProject
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/TryCompileProject
BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/tc
)
@@ -0,0 +1,2 @@
set(expect_lcov ControlFlow-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,30 @@
SF:<SRC>/ControlFlow.cmake
FN:16,rec
FN:26,${name}
FNDA:3,rec
FNDA:0,${name}
FNF:2
FNH:1
DA:1,1
DA:2,1
DA:3,3
DA:5,1
DA:6,3
DA:7,1
DA:9,2
DA:10,1
DA:13,1
DA:14,1
DA:16,1
DA:17,3
DA:18,2
DA:19,2
DA:21,3
DA:22,0
DA:24,1
DA:25,1
DA:26,1
DA:28,1
LF:20
LH:19
end_of_record
@@ -0,0 +1,28 @@
set(n 0)
while(n LESS 3)
math(EXPR n "${n} + 1")
endwhile()
foreach(i 1 2 3)
if(i EQUAL 2)
continue()
endif()
if(i EQUAL 3)
break()
endif()
endforeach()
block()
set(blk 1)
endblock()
function(rec k)
if(k GREATER 0)
math(EXPR k "${k} - 1")
rec(${k})
endif()
return()
set(never 1)
endfunction()
rec(2)
set(name dyn)
function(${name})
endfunction()
cmake_language(CALL set x 1)
@@ -0,0 +1,2 @@
set(expect_lcov CrossFile-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,24 @@
SF:<SRC>/CrossFile.cmake
FNF:0
FNH:0
DA:1,1
DA:2,1
DA:3,1
DA:4,1
LF:4
LH:4
end_of_record
SF:<SRC>/CrossFileLib.cmake
FN:1,lib_f
FN:4,lib_m
FNDA:2,lib_f
FNDA:1,lib_m
FNF:2
FNH:2
DA:1,1
DA:2,2
DA:4,1
DA:5,1
LF:4
LH:4
end_of_record
@@ -0,0 +1,4 @@
include(${CMAKE_CURRENT_LIST_DIR}/CrossFileLib.cmake)
lib_f()
lib_f()
lib_m()
@@ -0,0 +1,6 @@
function(lib_f)
set(f 1)
endfunction()
macro(lib_m)
set(m 1)
endmacro()
@@ -0,0 +1,4 @@
file(GLOB_RECURSE info_files "${RunCMake_TEST_BINARY_DIR}/*.info")
if(info_files)
set(RunCMake_TEST_FAILED "Unexpected coverage files:\n ${info_files}")
endif()
@@ -0,0 +1,4 @@
# On Windows an empty environment variable can read as undefined.
if(NOT CMAKE_HOST_WIN32 AND NOT DEFINED ENV{CMAKE_LISTFILE_COVERAGE_DIR})
message(FATAL_ERROR "CMAKE_LISTFILE_COVERAGE_DIR is not defined")
endif()
@@ -0,0 +1,3 @@
set(expect_lcov Eval-lcov.txt)
set(forbid_sf_regex ":EVAL")
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,7 @@
SF:<SRC>/Eval.cmake
FNF:0
FNH:0
DA:1,1
LF:1
LH:1
end_of_record
@@ -0,0 +1 @@
cmake_language(EVAL CODE "set(e 1)")
@@ -0,0 +1,2 @@
set(expect_lcov Exit-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,9 @@
SF:<SRC>/Exit.cmake
FNF:0
FNH:0
DA:1,1
DA:2,1
DA:3,0
LF:3
LH:2
end_of_record
@@ -0,0 +1 @@
3
@@ -0,0 +1,3 @@
set(a 1)
cmake_language(EXIT 3)
set(b 1)
@@ -0,0 +1,2 @@
set(expect_lcov FatalErrorNested-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,14 @@
SF:<SRC>/FatalErrorNested.cmake
FN:1,f
FNDA:1,f
FNF:1
FNH:1
DA:1,1
DA:2,1
DA:3,0
DA:5,1
DA:6,1
DA:7,0
LF:6
LH:4
end_of_record
@@ -0,0 +1 @@
1
@@ -0,0 +1 @@
boom
@@ -0,0 +1,8 @@
function(f)
message(FATAL_ERROR "boom")
set(a 1)
endfunction()
if(TRUE)
f()
set(b 1)
endif()
@@ -0,0 +1,2 @@
set(expect_lcov Functions-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,29 @@
SF:<SRC>/Functions.cmake
FN:1,F1
FN:4,m1
FN:7,unused
FN:11,dup@11
FN:14,dup@14
FNDA:2,F1
FNDA:1,m1
FNDA:0,unused
FNDA:1,dup@11
FNDA:0,dup@14
FNF:5
FNH:3
DA:1,1
DA:2,2
DA:4,1
DA:5,1
DA:7,1
DA:8,0
DA:10,1
DA:11,1
DA:14,0
DA:17,1
DA:18,1
DA:19,1
DA:20,1
LF:13
LH:11
end_of_record
@@ -0,0 +1,20 @@
function(F1)
set(x 1)
endfunction()
macro(m1)
set(y 1)
endmacro()
function(unused)
set(z 1)
endfunction()
if(TRUE)
function(dup)
endfunction()
else()
function(dup)
endfunction()
endif()
f1()
f1()
m1()
dup()
@@ -0,0 +1,2 @@
set(expect_lcov IncludeTwice-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,15 @@
SF:<SRC>/IncludeTwice.cmake
FNF:0
FNH:0
DA:1,1
DA:2,1
LF:2
LH:2
end_of_record
SF:<SRC>/IncludeTwiceLib.cmake
FNF:0
FNH:0
DA:1,2
LF:1
LH:1
end_of_record
@@ -0,0 +1,2 @@
include(${CMAKE_CURRENT_LIST_DIR}/IncludeTwiceLib.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/IncludeTwiceLib.cmake)
@@ -0,0 +1 @@
set(t 1)
@@ -0,0 +1,2 @@
set(expect_lcov Lines-lcov.txt)
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,19 @@
SF:<SRC>/Lines.cmake
FNF:0
FNH:0
DA:2,1
DA:4,1
DA:6,1
DA:7,0
DA:8,1
DA:9,0
DA:10,1
DA:11,1
DA:12,0
DA:13,0
DA:15,0
DA:17,1
DA:18,3
LF:13
LH:8
end_of_record
@@ -0,0 +1,21 @@
# comment
set(a 1)
set(b
2)
if(a EQUAL 2)
message(STATUS "two")
elseif(a EQUAL 3)
message(STATUS "three")
elseif(a EQUAL 1)
message(STATUS "one")
elseif(a EQUAL 4)
message(STATUS "four")
ELSE()
message(STATUS "other")
ENDIF()
foreach(i 1 2 3)
math(EXPR b "${b} + ${i}")
endforeach()
#[[ bracket
comment ]]
@@ -0,0 +1,3 @@
if(EXISTS "${no_cov_dir}")
set(RunCMake_TEST_FAILED "Coverage directory was created:\n ${no_cov_dir}")
endif()
@@ -0,0 +1 @@
1
@@ -0,0 +1,3 @@
set(expect_lcov ParseError-lcov.txt)
set(forbid_sf_regex "ParseErrorLib")
include(${RunCMake_SOURCE_DIR}/check-lcov.cmake)
@@ -0,0 +1,8 @@
SF:<SRC>/ParseError.cmake
FNF:0
FNH:0
DA:1,1
DA:2,1
LF:2
LH:2
end_of_record
@@ -0,0 +1 @@
1
@@ -0,0 +1 @@
ParseErrorLib\.cmake:1
@@ -0,0 +1,2 @@
set(a 1)
include(${CMAKE_CURRENT_LIST_DIR}/ParseErrorLib.cmake)
@@ -0,0 +1 @@
set(
@@ -0,0 +1 @@
1
@@ -0,0 +1 @@
^CMake Error: CMAKE_LISTFILE_COVERAGE_DIR is not an absolute path: "relative/dir"$
@@ -0,0 +1 @@
message(FATAL_ERROR "must not run")
@@ -0,0 +1,137 @@
cmake_minimum_required(VERSION 3.31)
include(RunCMake)
# run_coverage(<case> [COV_DIR <dir>] [COMMAND <run_cmake helper and args>])
#
# Run COMMAND, by default run_cmake_script(<case>), with the environment
# variable CMAKE_LISTFILE_COVERAGE_DIR set to COV_DIR, by default
# ${RunCMake_BINARY_DIR}/<case>-coverage. An absolute COV_DIR is removed
# first. A relative one is never removed. The variable cov_dir is set for
# <case>-check.cmake, which includes check-lcov.cmake.
function(run_coverage case)
cmake_parse_arguments(PARSE_ARGV 1 arg "" "COV_DIR" "COMMAND")
if(DEFINED arg_COV_DIR)
set(cov_dir "${arg_COV_DIR}")
else()
set(cov_dir "${RunCMake_BINARY_DIR}/${case}-coverage")
endif()
if(NOT arg_COMMAND)
set(arg_COMMAND run_cmake_script ${case})
endif()
if(IS_ABSOLUTE "${cov_dir}")
file(REMOVE_RECURSE "${cov_dir}")
endif()
if(DEFINED ENV{CMAKE_LISTFILE_COVERAGE_DIR})
set(had_old TRUE)
set(old "$ENV{CMAKE_LISTFILE_COVERAGE_DIR}")
else()
set(had_old FALSE)
endif()
set(ENV{CMAKE_LISTFILE_COVERAGE_DIR} "${cov_dir}")
list(POP_FRONT arg_COMMAND command)
cmake_language(CALL ${command} ${arg_COMMAND})
if(had_old)
set(ENV{CMAKE_LISTFILE_COVERAGE_DIR} "${old}")
else()
unset(ENV{CMAKE_LISTFILE_COVERAGE_DIR})
endif()
endfunction()
run_coverage(Lines)
run_coverage(Functions)
run_coverage(FatalErrorNested)
run_coverage(ControlFlow)
run_coverage(Callbacks)
run_coverage(CrossFile)
run_coverage(IncludeTwice)
run_coverage(Eval)
run_coverage(Exit)
run_coverage(ParseError)
run_coverage(Child COV_DIR "${RunCMake_BINARY_DIR}/Child cov dir")
run_coverage(CommentOnly)
run_coverage(CommentOnlyScript COMMAND run_cmake_command CommentOnlyScript
${CMAKE_COMMAND} -P ${RunCMake_SOURCE_DIR}/CommentOnlyLib.cmake)
# Startup errors. The script must not run.
run_coverage(RelativeDir COV_DIR "relative/dir")
file(WRITE "${RunCMake_BINARY_DIR}/BadDir-file" "")
block()
set(RunCMake_TEST_NOT_EXPECT_stderr "must not run")
run_coverage(BadDir COV_DIR "${RunCMake_BINARY_DIR}/BadDir-file/sub")
endblock()
# An empty value turns coverage off. set(ENV{...} "") would unset it.
run_cmake_command(EmptyDir ${CMAKE_COMMAND} -E env CMAKE_LISTFILE_COVERAGE_DIR=
${CMAKE_COMMAND} -P ${RunCMake_SOURCE_DIR}/EmptyDir.cmake)
# Configure and install a project.
run_coverage(Configure COMMAND run_cmake Configure)
block()
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/Configure-build)
set(RunCMake_TEST_NO_CLEAN 1)
run_coverage(Configure-install COMMAND run_cmake_command Configure-install
${CMAKE_COMMAND} --install . --prefix ${RunCMake_TEST_BINARY_DIR}/prefix)
endblock()
# Modes that do not record. The variable is set only in the child, so no
# driver process creates the directory.
function(run_no_record case dir)
set(RunCMake-check-file NoRecord-check.cmake)
if(IS_ABSOLUTE "${dir}")
set(no_cov_dir "${dir}")
file(REMOVE_RECURSE "${no_cov_dir}")
else()
set(no_cov_dir "${RunCMake_BINARY_DIR}/${case}-build/${dir}")
endif()
run_cmake_command(${case} ${CMAKE_COMMAND} -E env
CMAKE_LISTFILE_COVERAGE_DIR=${dir} ${ARGN})
endfunction()
run_no_record(NoRecord-E "${RunCMake_BINARY_DIR}/NoRecord-E-coverage"
${CMAKE_COMMAND} -E true)
block()
set(tree ${RunCMake_BINARY_DIR}/NoRecord-launch-build)
file(REMOVE_RECURSE "${tree}")
file(WRITE "${tree}/logs/CTestLaunchConfig.cmake" "set(launch_config 1)\n")
set(RunCMake_TEST_BINARY_DIR "${tree}")
set(RunCMake_TEST_NO_CLEAN 1)
run_no_record(NoRecord-launch "${RunCMake_BINARY_DIR}/NoRecord-launch-coverage"
CTEST_LAUNCH_LOGS=${tree}/logs ${CMAKE_CTEST_COMMAND} --launch
--build-dir ${tree} --output ${tree}/o -- ${CMAKE_COMMAND} -E false)
endblock()
run_no_record(NoRecord-version "relative/dir" ${CMAKE_COMMAND} --version)
run_no_record(NoRecord-ctest-version "relative/dir"
${CMAKE_CTEST_COMMAND} --version)
# The --check-build-system step of a build records only when it re-runs.
block()
set(src ${RunCMake_SOURCE_DIR}/CheckBuildSystem)
set(tree ${RunCMake_BINARY_DIR}/CheckBuildSystem-build)
set(RunCMake_TEST_SOURCE_DIR ${src})
set(RunCMake_TEST_BINARY_DIR ${tree})
run_cmake(CheckBuildSystem)
set(RunCMake_TEST_NO_CLEAN 1)
# CMake reads PreLoad.cmake from the binary dir too.
file(WRITE "${tree}/PreLoad.cmake" "set(preload 1)\n")
set(check_command ${CMAKE_COMMAND} -S ${src} -B ${tree}
--check-build-system ${tree}/check.cmake 0)
# The outputs are written last, so they are not older than the depends.
file(WRITE "${tree}/check.cmake" "
set(CMAKE_MAKEFILE_DEPENDS \"${tree}/depends.txt\")
set(CMAKE_MAKEFILE_OUTPUTS \"${tree}/outputs.txt\")
")
file(WRITE "${tree}/depends.txt" "")
file(WRITE "${tree}/outputs.txt" "")
run_coverage(CheckBuildSystem-uptodate
COMMAND run_cmake_command CheckBuildSystem-uptodate ${check_command})
# A missing product forces a re-configure.
file(APPEND "${tree}/check.cmake"
"set(CMAKE_MAKEFILE_PRODUCTS \"${tree}/missing.txt\")\n")
run_coverage(CheckBuildSystem-rerun
COMMAND run_cmake_command CheckBuildSystem-rerun ${check_command})
endblock()
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.31)
project(TryCompileProject NONE)
set(tc_line 1)
@@ -0,0 +1,150 @@
# Check the lcov tracefiles that a coverage run wrote.
#
# Inputs:
# cov_dir the coverage directory
# expect_files number of tracefiles, default 1, empty: not checked
# expect_lcov optional: name of a file in this directory that holds
# the full expected text of the records under
# RunCMake_SOURCE_DIR, with that prefix written as <SRC>
# expect_lcov_regex optional list: each must match that same text
# expect_sf_regex optional list: each must match the SF path of some record
# forbid_sf_regex optional list: none may match any SF path
#
# Records are split with string(FIND) and string(SUBSTRING), never with list
# operations, because paths and function names can hold ";" and "[".
function(check_lcov)
if(NOT DEFINED expect_files)
set(expect_files 1)
endif()
set(failed "")
file(GLOB entries LIST_DIRECTORIES true RELATIVE "${cov_dir}" "${cov_dir}/*")
list(LENGTH entries found)
set(info_files "")
# CMake regular expressions have no {8} quantifier.
string(REPEAT "[0-9a-f]" 8 token_regex)
foreach(entry IN LISTS entries)
if(entry MATCHES "^[0-9]+-${token_regex}\\.info$")
list(APPEND info_files "${entry}")
else()
string(APPEND failed "Unexpected entry in coverage directory: ${entry}\n")
endif()
endforeach()
if(NOT expect_files STREQUAL "" AND NOT found EQUAL expect_files)
string(APPEND failed
"expected ${expect_files} coverage file(s), found ${found}\n")
endif()
# Split the files into records.
set(n 0)
set(raw "")
foreach(name IN LISTS info_files)
file(READ "${cov_dir}/${name}" content)
string(APPEND raw "${content}")
while(1)
string(LENGTH "${content}" length)
if(length EQUAL 0)
break()
endif()
string(FIND "${content}" "end_of_record\n" pos)
if(pos EQUAL -1)
string(APPEND failed "${name}: text after the last end_of_record\n")
break()
endif()
math(EXPR end "${pos} + 14")
string(SUBSTRING "${content}" 0 ${end} record)
string(SUBSTRING "${content}" ${end} -1 content)
string(SUBSTRING "${record}" 0 3 head)
string(FIND "${record}" "\n" nl)
if(NOT head STREQUAL "SF:" OR nl LESS 3)
string(APPEND failed "${name}: record does not start with SF:\n")
break()
endif()
math(EXPR sf_length "${nl} - 3")
string(SUBSTRING "${record}" 3 ${sf_length} sf_${n})
set(record_${n} "${record}")
math(EXPR n "${n} + 1")
endwhile()
endforeach()
set(indexes "")
if(n GREATER 0)
math(EXPR last "${n} - 1")
foreach(i RANGE ${last})
list(APPEND indexes ${i})
endforeach()
endif()
foreach(regex IN LISTS expect_sf_regex)
set(matched FALSE)
foreach(i IN LISTS indexes)
if("${sf_${i}}" MATCHES "${regex}")
set(matched TRUE)
endif()
endforeach()
if(NOT matched)
string(APPEND failed "No SF path matches \"${regex}\"\n")
endif()
endforeach()
foreach(regex IN LISTS forbid_sf_regex)
foreach(i IN LISTS indexes)
if("${sf_${i}}" MATCHES "${regex}")
string(APPEND failed "SF path matches \"${regex}\": ${sf_${i}}\n")
endif()
endforeach()
endforeach()
# Keep the records under the source directory, with the prefix replaced.
set(prefix "${RunCMake_SOURCE_DIR}/")
string(LENGTH "${prefix}" prefix_length)
math(EXPR tail_start "${prefix_length} + 3")
set(kept "")
foreach(i IN LISTS indexes)
set(sf "${sf_${i}}")
set(match_prefix "${prefix}")
if(CMAKE_HOST_WIN32)
string(TOLOWER "${sf}" sf)
string(TOLOWER "${match_prefix}" match_prefix)
endif()
string(FIND "${sf}" "${match_prefix}" pos)
if(pos EQUAL 0)
string(SUBSTRING "${record_${i}}" ${tail_start} -1 tail)
set(record_${i} "SF:<SRC>/${tail}")
list(APPEND kept ${i})
endif()
endforeach()
# Sort the kept records by SF path and join them.
set(text "")
while(NOT "${kept}" STREQUAL "")
set(best "")
foreach(i IN LISTS kept)
if(best STREQUAL "" OR "${sf_${i}}" STRLESS "${sf_${best}}")
set(best ${i})
endif()
endforeach()
list(REMOVE_ITEM kept ${best})
string(APPEND text "${record_${best}}")
endwhile()
if(DEFINED expect_lcov)
file(READ "${RunCMake_SOURCE_DIR}/${expect_lcov}" expected)
string(REPLACE "\r\n" "\n" expected "${expected}")
if(NOT "${text}" STREQUAL "${expected}")
string(APPEND failed "Coverage does not match ${expect_lcov}.\n"
"Expected:\n${expected}Actual:\n${text}")
endif()
endif()
foreach(regex IN LISTS expect_lcov_regex)
if(NOT "${text}" MATCHES "${regex}")
string(APPEND failed "Coverage does not match \"${regex}\".\n")
endif()
endforeach()
if(NOT failed STREQUAL "")
string(APPEND failed "Coverage files:\n${raw}")
set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}${failed}" PARENT_SCOPE)
endif()
endfunction()
check_lcov()