blob: 79d4c7bceecb8b8f646211b5e5cdfef06363103a [file] [edit]
/* 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,
});
}