mirror of
https://github.com/Kitware/CMake.git
synced 2026-11-06 05:16:58 +00:00
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:
1 parent
173b0da32d
commit
37b38a29b6
86 files changed
+2100
-16
No files matched your search
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ set(CMakeLib_TESTS
|
||||
testGenExListPredicate.cxx
|
||||
testGenExListSortComparator.cxx
|
||||
testJSONHelpers.cxx
|
||||
testLcovTracefile.cxx
|
||||
testListFileCoverage.cxx
|
||||
testRST.cxx
|
||||
testRange.cxx
|
||||
testOptional.cxx
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user