mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
When a built-in cache variable is set via `-D <var>=<value>` with no help text supplied, populate `HELPSTRING` from the built-in documentation table (sourced from `Help/variable/*.rst` and the per-language pattern manuals). Project-defined and unknown variables continue to receive the legacy "No help, variable specified on the command line." sentinel. The cache entry's type and value are left untouched: type stays `UNINITIALIZED` for bare `-D` and an explicit `-D <var>:TYPE=<value>` is honored as before. The `cmCacheManager::LoadCache` path is not modified, so reload of existing caches is a fixed point. Fixes: #27830
497 lines
17 KiB
C++
497 lines
17 KiB
C++
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
file LICENSE.rst or https://cmake.org/licensing for details. */
|
|
|
|
#include <cstddef>
|
|
#include <string>
|
|
|
|
#include <cm/string_view>
|
|
|
|
#include "cmCacheDocumentationTable.h"
|
|
#include "cmCachePatternTable.h"
|
|
|
|
#include "testCommon.h"
|
|
|
|
namespace {
|
|
|
|
// The table is consulted by cmake::ProcessCacheArg via a binary search
|
|
// (std::lower_bound). That requires the entries to be sorted strictly
|
|
// ascending by Name in byte-wise (ASCII) order. These tests are the
|
|
// regression guard for that invariant against future hand-edits.
|
|
|
|
bool testNonEmpty()
|
|
{
|
|
ASSERT_TRUE(cmCacheDocumentationTable::EntriesSize() > 0);
|
|
ASSERT_TRUE(cmCacheDocumentationTable::EntriesBegin() !=
|
|
cmCacheDocumentationTable::EntriesEnd());
|
|
return true;
|
|
}
|
|
|
|
bool testSortedAndUnique()
|
|
{
|
|
auto const* const first = cmCacheDocumentationTable::EntriesBegin();
|
|
auto const* const last = cmCacheDocumentationTable::EntriesEnd();
|
|
for (auto const* it = first; it != last; ++it) {
|
|
if (it == first) {
|
|
continue;
|
|
}
|
|
auto const& prev = *(it - 1);
|
|
auto const& curr = *it;
|
|
if (!(prev.Name < curr.Name)) {
|
|
std::cout << "Entries are not strictly ascending: '" << prev.Name
|
|
<< "' is followed by '" << curr.Name << "' at index "
|
|
<< static_cast<std::size_t>(it - first) << '\n';
|
|
if (prev.Name == curr.Name) {
|
|
std::cout << " (duplicate Name)\n";
|
|
} else {
|
|
std::cout << " (out-of-order Name)\n";
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool testNonEmptyFields()
|
|
{
|
|
for (auto const* it = cmCacheDocumentationTable::EntriesBegin();
|
|
it != cmCacheDocumentationTable::EntriesEnd(); ++it) {
|
|
if (it->Name.empty()) {
|
|
std::cout << "Empty Name at index "
|
|
<< static_cast<std::size_t>(
|
|
it - cmCacheDocumentationTable::EntriesBegin())
|
|
<< '\n';
|
|
return false;
|
|
}
|
|
if (it->Summary.empty()) {
|
|
std::cout << "Empty Summary for entry '" << it->Name << "'\n";
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool testNameCharacters()
|
|
{
|
|
// Names must be printable ASCII without whitespace. Anything else
|
|
// would either be a copy/paste mishap or would cause std::lower_bound
|
|
// to behave in surprising ways relative to user input.
|
|
for (auto const* it = cmCacheDocumentationTable::EntriesBegin();
|
|
it != cmCacheDocumentationTable::EntriesEnd(); ++it) {
|
|
for (char const c : it->Name) {
|
|
auto const u = static_cast<unsigned char>(c);
|
|
if (u <= 0x20 || u >= 0x7f) {
|
|
std::cout << "Entry '" << it->Name
|
|
<< "' contains a non-printable or whitespace character\n";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool testSummaryIsSingleLine()
|
|
{
|
|
// Summaries are rendered as one-line tooltips in cmake-gui / ccmake.
|
|
// Embedded newlines or carriage returns break that rendering.
|
|
for (auto const* it = cmCacheDocumentationTable::EntriesBegin();
|
|
it != cmCacheDocumentationTable::EntriesEnd(); ++it) {
|
|
for (char const c : it->Summary) {
|
|
if (c == '\n' || c == '\r') {
|
|
std::cout << "Summary for entry '" << it->Name
|
|
<< "' contains an embedded newline or carriage return\n";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool testGetRoundTrip()
|
|
{
|
|
// For every entry in the table, Get(Name) must return the same Summary.
|
|
// This catches a regression where the binary-search predicate or the
|
|
// sort invariant silently drifts apart.
|
|
for (auto const* it = cmCacheDocumentationTable::EntriesBegin();
|
|
it != cmCacheDocumentationTable::EntriesEnd(); ++it) {
|
|
auto const got = cmCacheDocumentationTable::Get(it->Name);
|
|
if (got.Summary != it->Summary) {
|
|
std::cout << "Get(\"" << it->Name
|
|
<< "\").Summary did not round-trip to the table's Summary\n";
|
|
return false;
|
|
}
|
|
}
|
|
// And an obviously-unknown key must return the sentinel.
|
|
auto const missing = cmCacheDocumentationTable::Get(
|
|
cm::string_view("__definitely_not_a_cmake_variable__"));
|
|
if (!missing.Summary.empty()) {
|
|
std::cout << "Get() returned a non-empty Summary for an unknown key\n";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pattern table tests.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The pattern table is consulted by cmCacheDocumentationTable::Get on the
|
|
// exact-match miss path. The tests below pin down its structural
|
|
// invariants and the most important matcher behaviors (lazy
|
|
// lookahead-anchored matching, fixed-length digit classes, exact wins over
|
|
// pattern, permissive matching with restriction-in-prose).
|
|
|
|
// Count the angle-bracketed placeholders in a pattern string. A
|
|
// placeholder is the inclusive run from a ``<`` to the next ``>``.
|
|
std::size_t CountPlaceholders(cm::string_view pattern)
|
|
{
|
|
std::size_t n = 0;
|
|
for (std::size_t i = 0; i < pattern.size(); ++i) {
|
|
if (pattern[i] == '<') {
|
|
++n;
|
|
auto const close = pattern.find('>', i);
|
|
if (close == cm::string_view::npos) {
|
|
return n; // malformed; let testPatternTableFieldsValid flag it
|
|
}
|
|
i = close;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// Total length of the literal (non-placeholder) characters in a pattern.
|
|
std::size_t LiteralLength(cm::string_view pattern)
|
|
{
|
|
std::size_t total = 0;
|
|
for (std::size_t i = 0; i < pattern.size(); ++i) {
|
|
if (pattern[i] == '<') {
|
|
auto const close = pattern.find('>', i);
|
|
if (close == cm::string_view::npos) {
|
|
break;
|
|
}
|
|
i = close;
|
|
continue;
|
|
}
|
|
++total;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
bool testPatternTableNonEmpty()
|
|
{
|
|
ASSERT_TRUE(cmCachePatternTable::EntriesSize() > 0);
|
|
ASSERT_TRUE(cmCachePatternTable::EntriesBegin() !=
|
|
cmCachePatternTable::EntriesEnd());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternTableFieldsValid()
|
|
{
|
|
// Per-entry invariants: non-empty Pattern and Summary, at least one
|
|
// placeholder, single-line printable ASCII Summary of bounded length,
|
|
// and (a stricter rule derived from the matcher design) no two adjacent
|
|
// placeholders with no separating literal -- the matcher's
|
|
// lookahead-anchored semantics require a literal anchor between
|
|
// placeholders to be unambiguous.
|
|
for (auto const* it = cmCachePatternTable::EntriesBegin();
|
|
it != cmCachePatternTable::EntriesEnd(); ++it) {
|
|
if (it->Pattern.empty()) {
|
|
std::cout << "Empty Pattern at index "
|
|
<< static_cast<std::size_t>(
|
|
it - cmCachePatternTable::EntriesBegin())
|
|
<< '\n';
|
|
return false;
|
|
}
|
|
if (it->Summary.empty()) {
|
|
std::cout << "Empty Summary for pattern '" << it->Pattern << "'\n";
|
|
return false;
|
|
}
|
|
if (CountPlaceholders(it->Pattern) == 0) {
|
|
std::cout << "Pattern '" << it->Pattern
|
|
<< "' has no <...> placeholder; "
|
|
"use cmCacheDocumentationTable for exact-match entries\n";
|
|
return false;
|
|
}
|
|
if (it->Summary.size() > 200) {
|
|
std::cout << "Summary for pattern '" << it->Pattern
|
|
<< "' exceeds the 200-character tooltip limit\n";
|
|
return false;
|
|
}
|
|
for (char const c : it->Summary) {
|
|
auto const u = static_cast<unsigned char>(c);
|
|
if (u < 0x20 || u >= 0x7f) {
|
|
std::cout << "Summary for pattern '" << it->Pattern
|
|
<< "' contains a non-printable or non-ASCII character\n";
|
|
return false;
|
|
}
|
|
}
|
|
// Adjacent placeholders are forbidden (matcher cannot disambiguate
|
|
// them without a separating literal anchor). Look for ``><``.
|
|
for (std::size_t i = 1; i < it->Pattern.size(); ++i) {
|
|
if (it->Pattern[i] == '<' && it->Pattern[i - 1] == '>') {
|
|
std::cout << "Pattern '" << it->Pattern
|
|
<< "' contains adjacent placeholders; insert a literal "
|
|
"separator between them\n";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool testPatternTableOrdered()
|
|
{
|
|
// kPatterns[] must obey ``(numPlaceholders ASC, totalLiteralLen DESC,
|
|
// Pattern ASC)``. This determinism guarantees that a contributor
|
|
// re-ordering the array cannot silently flip which of several equally
|
|
// valid pattern matches wins for a given input.
|
|
auto const* const first = cmCachePatternTable::EntriesBegin();
|
|
auto const* const last = cmCachePatternTable::EntriesEnd();
|
|
for (auto const* it = first; it != last; ++it) {
|
|
if (it == first) {
|
|
continue;
|
|
}
|
|
auto const& prev = *(it - 1);
|
|
auto const& curr = *it;
|
|
auto const np_prev = CountPlaceholders(prev.Pattern);
|
|
auto const np_curr = CountPlaceholders(curr.Pattern);
|
|
auto const ll_prev = LiteralLength(prev.Pattern);
|
|
auto const ll_curr = LiteralLength(curr.Pattern);
|
|
bool ordered;
|
|
if (np_prev != np_curr) {
|
|
ordered = np_prev < np_curr;
|
|
} else if (ll_prev != ll_curr) {
|
|
ordered = ll_prev > ll_curr;
|
|
} else {
|
|
ordered = prev.Pattern < curr.Pattern;
|
|
}
|
|
if (!ordered) {
|
|
std::cout << "Pattern table out of canonical order between '"
|
|
<< prev.Pattern << "' (placeholders=" << np_prev
|
|
<< ", literal_len=" << ll_prev << ") and '" << curr.Pattern
|
|
<< "' (placeholders=" << np_curr << ", literal_len=" << ll_curr
|
|
<< "). Expected (numPlaceholders ASC, totalLiteralLen DESC, "
|
|
"Pattern ASC).\n";
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool testPatternMatchLangPositive()
|
|
{
|
|
// CMAKE_<LANG>_CLANG_TIDY must match for several different <LANG>
|
|
// values, and all must return the same static Summary pointer (the
|
|
// permissive matcher returns the pattern entry's storage verbatim).
|
|
auto const cxx = cmCacheDocumentationTable::Get("CMAKE_CXX_CLANG_TIDY");
|
|
auto const hip = cmCacheDocumentationTable::Get("CMAKE_HIP_CLANG_TIDY");
|
|
auto const fortran =
|
|
cmCacheDocumentationTable::Get("CMAKE_Fortran_CLANG_TIDY");
|
|
ASSERT_TRUE(!cxx.Summary.empty());
|
|
ASSERT_TRUE(!hip.Summary.empty());
|
|
ASSERT_TRUE(!fortran.Summary.empty());
|
|
// Same static-storage Summary pointer.
|
|
ASSERT_TRUE(cxx.Summary.data() == hip.Summary.data());
|
|
ASSERT_TRUE(cxx.Summary.data() == fortran.Summary.data());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternMatchConfigPositive()
|
|
{
|
|
// CMAKE_<LANG>_FLAGS_<CONFIG> matches both standard configurations and
|
|
// user-defined ones (the matcher does not enforce a canonical CONFIG
|
|
// set; it accepts any identifier).
|
|
auto const debug = cmCacheDocumentationTable::Get("CMAKE_CXX_FLAGS_DEBUG");
|
|
auto const release =
|
|
cmCacheDocumentationTable::Get("CMAKE_CXX_FLAGS_RELEASE");
|
|
auto const coverage =
|
|
cmCacheDocumentationTable::Get("CMAKE_CXX_FLAGS_COVERAGE");
|
|
ASSERT_TRUE(!debug.Summary.empty());
|
|
ASSERT_TRUE(!release.Summary.empty());
|
|
ASSERT_TRUE(!coverage.Summary.empty());
|
|
ASSERT_TRUE(debug.Summary.data() == release.Summary.data());
|
|
ASSERT_TRUE(debug.Summary.data() == coverage.Summary.data());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternMatchNNNN()
|
|
{
|
|
// <NNNN> is exactly 4 ASCII digits. 3-digit, 5-digit, and non-digit
|
|
// inputs must all miss.
|
|
ASSERT_TRUE(!cmCacheDocumentationTable::Get("CMAKE_POLICY_DEFAULT_CMP0048")
|
|
.Summary.empty());
|
|
ASSERT_TRUE(cmCacheDocumentationTable::Get("CMAKE_POLICY_DEFAULT_CMP48")
|
|
.Summary.empty());
|
|
ASSERT_TRUE(cmCacheDocumentationTable::Get("CMAKE_POLICY_DEFAULT_CMP00048")
|
|
.Summary.empty());
|
|
ASSERT_TRUE(cmCacheDocumentationTable::Get("CMAKE_POLICY_DEFAULT_CMPxyz0")
|
|
.Summary.empty());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternMatchN()
|
|
{
|
|
// <n> is exactly 1 ASCII digit. Multi-digit and non-digit must miss.
|
|
ASSERT_TRUE(
|
|
!cmCacheDocumentationTable::Get("CMAKE_MATCH_5").Summary.empty());
|
|
ASSERT_TRUE(
|
|
cmCacheDocumentationTable::Get("CMAKE_MATCH_12").Summary.empty());
|
|
ASSERT_TRUE(cmCacheDocumentationTable::Get("CMAKE_MATCH_a").Summary.empty());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternMatchMultiPlaceholder()
|
|
{
|
|
// CMAKE_CXX_FLAGS_DEBUG must match the two-placeholder pattern
|
|
// ``CMAKE_<LANG>_FLAGS_<CONFIG>``, not the one-placeholder pattern
|
|
// ``CMAKE_<LANG>_FLAGS`` (which fails full-input consumption on a
|
|
// longer input by design).
|
|
auto const got = cmCacheDocumentationTable::Get("CMAKE_CXX_FLAGS_DEBUG");
|
|
ASSERT_TRUE(!got.Summary.empty());
|
|
// Locate the two-placeholder entry and assert pointer equality.
|
|
cm::string_view two_ph_summary;
|
|
for (auto const* it = cmCachePatternTable::EntriesBegin();
|
|
it != cmCachePatternTable::EntriesEnd(); ++it) {
|
|
if (it->Pattern == "CMAKE_<LANG>_FLAGS_<CONFIG>") {
|
|
two_ph_summary = it->Summary;
|
|
break;
|
|
}
|
|
}
|
|
ASSERT_TRUE(!two_ph_summary.empty());
|
|
ASSERT_TRUE(got.Summary.data() == two_ph_summary.data());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternMatchPackage()
|
|
{
|
|
// The single <PackageName>_ROOT pattern covers both mixed-case
|
|
// (CMP0074) and upper-case (CMP0144) spellings: the identifier
|
|
// lexical class ``[A-Za-z][A-Za-z0-9_]*`` accepts both.
|
|
ASSERT_TRUE(!cmCacheDocumentationTable::Get("Boost_ROOT").Summary.empty());
|
|
ASSERT_TRUE(!cmCacheDocumentationTable::Get("Qt6_ROOT").Summary.empty());
|
|
ASSERT_TRUE(!cmCacheDocumentationTable::Get("BOOST_ROOT").Summary.empty());
|
|
return true;
|
|
}
|
|
|
|
bool testExactWinsOverPattern()
|
|
{
|
|
// CMAKE_CUDA_STANDARD is in the exact-match table and would also match
|
|
// the pattern CMAKE_<LANG>_STANDARD. The exact entry must win.
|
|
auto const got = cmCacheDocumentationTable::Get("CMAKE_CUDA_STANDARD");
|
|
ASSERT_TRUE(!got.Summary.empty());
|
|
// Find the exact entry's Summary in cmCacheDocumentationTable.
|
|
cm::string_view exact_summary;
|
|
for (auto const* it = cmCacheDocumentationTable::EntriesBegin();
|
|
it != cmCacheDocumentationTable::EntriesEnd(); ++it) {
|
|
if (it->Name == "CMAKE_CUDA_STANDARD") {
|
|
exact_summary = it->Summary;
|
|
break;
|
|
}
|
|
}
|
|
ASSERT_TRUE(!exact_summary.empty());
|
|
// Pointer equality: the exact-match path returns the table's static
|
|
// storage; the pattern-match path would return cmCachePatternTable's
|
|
// (different) storage.
|
|
ASSERT_TRUE(got.Summary.data() == exact_summary.data());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternMatchNegativeUnknown()
|
|
{
|
|
// A name that matches no pattern at all returns the sentinel.
|
|
auto const got =
|
|
cmCacheDocumentationTable::Get("CMAKE_TOTAL_NONSENSE_VARIABLE_XYZZY");
|
|
ASSERT_TRUE(got.Summary.empty());
|
|
return true;
|
|
}
|
|
|
|
bool testPatternUnderscoreInLang()
|
|
{
|
|
// The lazy matcher allows ``<LANG>`` to contain underscores (the
|
|
// identifier class is ``[A-Za-z][A-Za-z0-9_]*``); the lookahead-anchor
|
|
// search finds the right boundary. This pins the design decision:
|
|
// permissive matching with restriction-in-prose, not regex-strict
|
|
// language enforcement. A made-up ``MY_WEIRD_LANG`` is still picked
|
|
// up by the CMAKE_<LANG>_CLANG_TIDY pattern.
|
|
auto const got =
|
|
cmCacheDocumentationTable::Get("CMAKE_MY_WEIRD_LANG_CLANG_TIDY");
|
|
ASSERT_TRUE(!got.Summary.empty());
|
|
return true;
|
|
}
|
|
|
|
bool testEveryPatternReachable()
|
|
{
|
|
// For each pattern we synthesize a canonical instantiation by
|
|
// replacing each placeholder with a class-appropriate token:
|
|
// ``Aa`` for identifier-class placeholders (LANG / CONFIG /
|
|
// PackageName / PROJECT-NAME / an-attribute / FEATURE / TYPE),
|
|
// ``0000`` for the fixed-length ``<NNNN>``, and ``0`` for the
|
|
// fixed-length ``<n>``. The matcher's lazy + lookahead-anchored
|
|
// semantics then either commit (Summary non-empty -> reachable) or
|
|
// reject (Summary empty -> dead pattern, test fails).
|
|
for (auto const* it = cmCachePatternTable::EntriesBegin();
|
|
it != cmCachePatternTable::EntriesEnd(); ++it) {
|
|
std::string instance;
|
|
cm::string_view const pat = it->Pattern;
|
|
for (std::size_t i = 0; i < pat.size();) {
|
|
if (pat[i] != '<') {
|
|
instance.push_back(pat[i]);
|
|
++i;
|
|
continue;
|
|
}
|
|
auto const close = pat.find('>', i);
|
|
if (close == cm::string_view::npos) {
|
|
std::cout << "Malformed pattern '" << pat << "' (unterminated '<')\n";
|
|
return false;
|
|
}
|
|
cm::string_view const ph = pat.substr(i, close - i + 1);
|
|
if (ph == "<NNNN>") {
|
|
instance += "0000";
|
|
} else if (ph == "<n>") {
|
|
instance += "0";
|
|
} else {
|
|
// Identifier-class placeholder (Ident or IdentHyphen). ``Aa``
|
|
// satisfies both lexical classes and is short enough not to
|
|
// accidentally collide with any anchor literal in the table.
|
|
instance += "Aa";
|
|
}
|
|
i = close + 1;
|
|
}
|
|
auto const got = cmCacheDocumentationTable::Get(instance);
|
|
if (got.Summary.empty()) {
|
|
std::cout << "Dead pattern '" << pat << "': canonical instantiation '"
|
|
<< instance
|
|
<< "' is not matched by any entry in the pattern table\n";
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
int testCacheDocumentationTable(int /*unused*/, char* /*unused*/[])
|
|
{
|
|
return runTests({
|
|
testNonEmpty,
|
|
testSortedAndUnique,
|
|
testNonEmptyFields,
|
|
testNameCharacters,
|
|
testSummaryIsSingleLine,
|
|
testGetRoundTrip,
|
|
// Pattern-table cases.
|
|
testPatternTableNonEmpty,
|
|
testPatternTableFieldsValid,
|
|
testPatternTableOrdered,
|
|
testPatternMatchLangPositive,
|
|
testPatternMatchConfigPositive,
|
|
testPatternMatchNNNN,
|
|
testPatternMatchN,
|
|
testPatternMatchMultiPlaceholder,
|
|
testPatternMatchPackage,
|
|
testExactWinsOverPattern,
|
|
testPatternMatchNegativeUnknown,
|
|
testPatternUnderscoreInLang,
|
|
testEveryPatternReachable,
|
|
});
|
|
}
|