| /* 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, |
| }); |
| } |