From 949a1e120a3c2d9ecac08f7240a27d307fad503f Mon Sep 17 00:00:00 2001
From: Alex Turbov <i.zaufi@gmail.com>
Date: Tue, 13 Aug 2019 01:25:07 +0300
Subject: message: New message types to mark checks performed by CMake

Closes #19638.

Co-Authored-By: Craig Scott <craig.scott@crascit.com>
---
 Help/command/message.rst                         | 104 ++++++++++++++++++++-
 Help/release/dev/new-message-types.rst           |   5 +
 Source/cmMessageCommand.cxx                      | 113 ++++++++++++++++++-----
 Source/cmake.h                                   |  24 +++++
 Tests/RunCMake/message/RunCMakeTest.cmake        |   5 +
 Tests/RunCMake/message/message-checks-stderr.txt |   3 +
 Tests/RunCMake/message/message-checks-stdout.txt |  10 ++
 Tests/RunCMake/message/message-checks.cmake      |  13 +++
 8 files changed, 250 insertions(+), 27 deletions(-)
 create mode 100644 Help/release/dev/new-message-types.rst
 create mode 100644 Tests/RunCMake/message/message-checks-stderr.txt
 create mode 100644 Tests/RunCMake/message/message-checks-stdout.txt
 create mode 100644 Tests/RunCMake/message/message-checks.cmake

diff --git a/Help/command/message.rst b/Help/command/message.rst
index beb820a..6bc0e4c 100644
--- a/Help/command/message.rst
+++ b/Help/command/message.rst
@@ -1,13 +1,33 @@
 message
 -------
 
-Display a message to the user.
+Log a message.
+
+Synopsis
+^^^^^^^^
+
+.. parsed-literal::
+
+  `General messages`_
+    message([<mode>] "message text" ...)
+
+  `Reporting checks`_
+    message(<checkState> "message text" ...)
+
+
+General messages
+^^^^^^^^^^^^^^^^
 
 .. code-block:: cmake
 
-  message([<mode>] "message to display" ...)
+  message([<mode>] "message text" ...)
+
+Record the specified message text in the log.  If more than one message
+string is given, they are concatenated into a single message with no
+separator between the strings.
 
-The optional ``<mode>`` keyword determines the type of message:
+The optional ``<mode>`` keyword determines the type of message, which
+influences the way the message is handled:
 
 ``FATAL_ERROR``
   CMake Error, stop processing and generation.
@@ -82,3 +102,81 @@ usage examples.
 CMake Warning and Error message text displays using a simple markup
 language.  Non-indented text is formatted in line-wrapped paragraphs
 delimited by newlines.  Indented text is considered pre-formatted.
+
+
+Reporting checks
+^^^^^^^^^^^^^^^^
+
+A common pattern in CMake output is a message indicating the start of some
+sort of check, followed by another message reporting the result of that check.
+For example:
+
+.. code-block:: cmake
+
+  message(STATUS "Looking for someheader.h")
+  #... do the checks, set checkSuccess with the result
+  if(checkSuccess)
+    message(STATUS "Looking for someheader.h - found")
+  else()
+    message(STATUS "Looking for someheader.h - not found")
+  endif()
+
+This can be more robustly and conveniently expressed using the ``CHECK_...``
+keyword form of the ``message()`` command:
+
+.. code-block:: cmake
+
+  message(<checkState> "message" ...)
+
+where ``<checkState>`` must be one of the following:
+
+  ``CHECK_START``
+    Record a concise message about the check about to be performed.
+
+  ``CHECK_PASS``
+    Record a successful result for a check.
+
+  ``CHECK_FAIL``
+    Record an unsuccessful result for a check.
+
+When recording a check result, the command repeats the message from the most
+recently started check for which no result has yet been reported, then some
+separator characters and then the message text provided after the
+``CHECK_PASS`` or ``CHECK_FAIL`` keyword.  Check messages are always reported
+at ``STATUS`` log level.
+
+Checks may be nested and every ``CHECK_START`` should have exactly one
+matching ``CHECK_PASS`` or ``CHECK_FAIL``.
+The :variable:`CMAKE_MESSAGE_INDENT` variable can also be used to add
+indenting to nested checks if desired.  For example:
+
+.. code-block:: cmake
+
+  message(CHECK_START "Finding my things")
+  list(APPEND CMAKE_MESSAGE_INDENT "  ")
+  unset(missingComponents)
+
+  message(CHECK_START "Finding partA")
+  # ... do check, assume we find A
+  message(CHECK_PASS "found")
+
+  message(CHECK_START "Finding partB")
+  # ... do check, assume we don't find B
+  list(APPEND missingComponents B)
+  message(CHECK_FAIL "not found")
+
+  list(POP_BACK CMAKE_MESSAGE_INDENT)
+  if(missingComponents)
+    message(CHECK_FAIL "missing components: ${missingComponents}")
+  else()
+    message(CHECK_PASS "all components found")
+  endif()
+
+Output from the above would appear something like the following::
+
+  -- Finding my things
+  --   Finding partA
+  --   Finding partA - found
+  --   Finding partB
+  --   Finding partB - not found
+  -- Finding my things - missing components: B
diff --git a/Help/release/dev/new-message-types.rst b/Help/release/dev/new-message-types.rst
new file mode 100644
index 0000000..8f164b9
--- /dev/null
+++ b/Help/release/dev/new-message-types.rst
@@ -0,0 +1,5 @@
+new-message-types
+-----------------
+
+* The :command:`message` command gained new keywords ``CHECK_START``,
+  ``CHECK_PASS`` and ``CHECK_FAIL``.
diff --git a/Source/cmMessageCommand.cxx b/Source/cmMessageCommand.cxx
index 24ac71a..bf8183b 100644
--- a/Source/cmMessageCommand.cxx
+++ b/Source/cmMessageCommand.cxx
@@ -3,6 +3,11 @@
 #include "cmMessageCommand.h"
 
 #include <cassert>
+#include <utility>
+
+#include <cm/string_view>
+
+#include "cm_static_string_view.hxx"
 
 #include "cmExecutionStatus.h"
 #include "cmMakefile.h"
@@ -13,6 +18,55 @@
 #include "cmSystemTools.h"
 #include "cmake.h"
 
+namespace {
+
+enum class CheckingType
+{
+  UNDEFINED,
+  CHECK_START,
+  CHECK_PASS,
+  CHECK_FAIL
+};
+
+std::string IndentText(std::string text, cmMakefile& mf)
+{
+  auto indent =
+    cmJoin(cmExpandedList(mf.GetSafeDefinition("CMAKE_MESSAGE_INDENT")), "");
+
+  const auto showContext = mf.GetCMakeInstance()->GetShowLogContext() ||
+    mf.IsOn("CMAKE_MESSAGE_CONTEXT_SHOW");
+  if (showContext) {
+    auto context = cmJoin(
+      cmExpandedList(mf.GetSafeDefinition("CMAKE_MESSAGE_CONTEXT")), ".");
+    if (!context.empty()) {
+      indent.insert(0u, cmStrCat("["_s, context, "] "_s));
+    }
+  }
+
+  if (!indent.empty()) {
+    cmSystemTools::ReplaceString(text, "\n", "\n" + indent);
+    text.insert(0u, indent);
+  }
+  return text;
+}
+
+void ReportCheckResult(cm::string_view what, std::string result,
+                       cmMakefile& mf)
+{
+  if (mf.GetCMakeInstance()->HasCheckInProgress()) {
+    auto text = mf.GetCMakeInstance()->GetTopCheckInProgressMessage() + " - " +
+      std::move(result);
+    mf.DisplayStatus(IndentText(std::move(text), mf), -1);
+  } else {
+    mf.GetMessenger()->DisplayMessage(
+      MessageType::AUTHOR_WARNING,
+      cmStrCat("Ignored "_s, what, " without CHECK_START"_s),
+      mf.GetBacktrace());
+  }
+}
+
+} // anonymous namespace
+
 // cmLibraryCommand
 bool cmMessageCommand(std::vector<std::string> const& args,
                       cmExecutionStatus& status)
@@ -29,6 +83,7 @@ bool cmMessageCommand(std::vector<std::string> const& args,
   auto type = MessageType::MESSAGE;
   auto fatal = false;
   auto level = cmake::LogLevel::LOG_UNDEFINED;
+  auto checkingType = CheckingType::UNDEFINED;
   if (*i == "SEND_ERROR") {
     type = MessageType::FATAL_ERROR;
     level = cmake::LogLevel::LOG_ERROR;
@@ -55,6 +110,18 @@ bool cmMessageCommand(std::vector<std::string> const& args,
       return true;
     }
     ++i;
+  } else if (*i == "CHECK_START") {
+    level = cmake::LogLevel::LOG_STATUS;
+    checkingType = CheckingType::CHECK_START;
+    ++i;
+  } else if (*i == "CHECK_PASS") {
+    level = cmake::LogLevel::LOG_STATUS;
+    checkingType = CheckingType::CHECK_PASS;
+    ++i;
+  } else if (*i == "CHECK_FAIL") {
+    level = cmake::LogLevel::LOG_STATUS;
+    checkingType = CheckingType::CHECK_FAIL;
+    ++i;
   } else if (*i == "STATUS") {
     level = cmake::LogLevel::LOG_STATUS;
     ++i;
@@ -111,28 +178,6 @@ bool cmMessageCommand(std::vector<std::string> const& args,
 
   auto message = cmJoin(cmMakeRange(i, args.cend()), "");
 
-  if (cmake::LogLevel::LOG_NOTICE <= level) {
-    auto indent =
-      cmJoin(cmExpandedList(mf.GetSafeDefinition("CMAKE_MESSAGE_INDENT")), "");
-    if (!indent.empty()) {
-      cmSystemTools::ReplaceString(message, "\n", "\n" + indent);
-      message = indent + message;
-    }
-
-    const auto showContext = mf.GetCMakeInstance()->GetShowLogContext() ||
-      mf.IsOn("CMAKE_MESSAGE_CONTEXT_SHOW");
-    if (showContext) {
-      // Output the current context (if any)
-      auto context = cmJoin(
-        cmExpandedList(mf.GetSafeDefinition("CMAKE_MESSAGE_CONTEXT")), ".");
-      if (!context.empty()) {
-        context = "[" + context + "] ";
-        cmSystemTools::ReplaceString(message, "\n", "\n" + context);
-        message = context + message;
-      }
-    }
-  }
-
   switch (level) {
     case cmake::LogLevel::LOG_ERROR:
     case cmake::LogLevel::LOG_WARNING:
@@ -141,14 +186,34 @@ bool cmMessageCommand(std::vector<std::string> const& args,
       break;
 
     case cmake::LogLevel::LOG_NOTICE:
-      cmSystemTools::Message(message);
+      cmSystemTools::Message(IndentText(message, mf));
       break;
 
     case cmake::LogLevel::LOG_STATUS:
+      switch (checkingType) {
+        case CheckingType::CHECK_START:
+          mf.DisplayStatus(IndentText(message, mf), -1);
+          mf.GetCMakeInstance()->PushCheckInProgressMessage(message);
+          break;
+
+        case CheckingType::CHECK_PASS:
+          ReportCheckResult("CHECK_PASS"_s, message, mf);
+          break;
+
+        case CheckingType::CHECK_FAIL:
+          ReportCheckResult("CHECK_FAIL"_s, message, mf);
+          break;
+
+        default:
+          mf.DisplayStatus(IndentText(message, mf), -1);
+          break;
+      }
+      break;
+
     case cmake::LogLevel::LOG_VERBOSE:
     case cmake::LogLevel::LOG_DEBUG:
     case cmake::LogLevel::LOG_TRACE:
-      mf.DisplayStatus(message, -1);
+      mf.DisplayStatus(IndentText(message, mf), -1);
       break;
 
     default:
diff --git a/Source/cmake.h b/Source/cmake.h
index c2f2cce..9e78436 100644
--- a/Source/cmake.h
+++ b/Source/cmake.h
@@ -5,12 +5,15 @@
 
 #include "cmConfigure.h" // IWYU pragma: keep
 
+#include <cstddef>
 #include <functional>
 #include <map>
 #include <memory>
 #include <set>
+#include <stack>
 #include <string>
 #include <unordered_set>
+#include <utility>
 #include <vector>
 
 #include "cmGeneratedFileStream.h"
@@ -387,6 +390,25 @@ public:
   void SetLogLevel(LogLevel level) { this->MessageLogLevel = level; }
   static LogLevel StringToLogLevel(const std::string& levelStr);
 
+  bool HasCheckInProgress() const
+  {
+    return !this->CheckInProgressMessages.empty();
+  }
+  std::size_t GetCheckInProgressSize() const
+  {
+    return this->CheckInProgressMessages.size();
+  }
+  std::string GetTopCheckInProgressMessage()
+  {
+    auto message = this->CheckInProgressMessages.top();
+    this->CheckInProgressMessages.pop();
+    return message;
+  }
+  void PushCheckInProgressMessage(std::string message)
+  {
+    this->CheckInProgressMessages.emplace(std::move(message));
+  }
+
   //! Do we want debug output during the cmake run.
   bool GetDebugOutput() { return this->DebugOutput; }
   void SetDebugOutputOn(bool b) { this->DebugOutput = b; }
@@ -596,6 +618,8 @@ private:
   bool LogLevelWasSetViaCLI = false;
   bool LogContext = false;
 
+  std::stack<std::string> CheckInProgressMessages;
+
   void UpdateConversionPathTable();
 
   //! Print a list of valid generators to stderr.
diff --git a/Tests/RunCMake/message/RunCMakeTest.cmake b/Tests/RunCMake/message/RunCMakeTest.cmake
index bf6a47e..1f3fa12 100644
--- a/Tests/RunCMake/message/RunCMakeTest.cmake
+++ b/Tests/RunCMake/message/RunCMakeTest.cmake
@@ -83,3 +83,8 @@ run_cmake_command(
     message-context-cli-wins-cache
     ${CMAKE_COMMAND} --log-level=verbose --log-context -DCMAKE_MESSAGE_CONTEXT_SHOW=OFF -P ${RunCMake_SOURCE_DIR}/message-context.cmake
   )
+
+run_cmake_command(
+    message-checks
+    ${CMAKE_COMMAND} -P ${RunCMake_SOURCE_DIR}/message-checks.cmake
+  )
diff --git a/Tests/RunCMake/message/message-checks-stderr.txt b/Tests/RunCMake/message/message-checks-stderr.txt
new file mode 100644
index 0000000..fdacdb2
--- /dev/null
+++ b/Tests/RunCMake/message/message-checks-stderr.txt
@@ -0,0 +1,3 @@
+^CMake Warning \(dev\) at.*/Tests/RunCMake/message/message-checks.cmake:13 \(message\):
+  Ignored CHECK_FAIL without CHECK_START
+This warning is for project developers.  Use -Wno-dev to suppress it.$
diff --git a/Tests/RunCMake/message/message-checks-stdout.txt b/Tests/RunCMake/message/message-checks-stdout.txt
new file mode 100644
index 0000000..4f5f2ef
--- /dev/null
+++ b/Tests/RunCMake/message/message-checks-stdout.txt
@@ -0,0 +1,10 @@
+-- Find `libfoo`
+-- Looking for `libfoo\.h`
+-- Looking for `libfoo\.h` - found \[/usr/include\]
+-- Looking for `libfoo\.so`
+-- Looking for `libfoo\.so` - found \[/usr/lib/libfoo\.so\]
+-- Getting `libfoo` version
+-- Looking for `libfoo/version\.h`
+-- Looking for `libfoo/version\.h` - found
+-- Getting `libfoo` version - 1\.2\.3
+-- Find `libfoo` - required version 4\.5\.6 but found 1\.2\.3
diff --git a/Tests/RunCMake/message/message-checks.cmake b/Tests/RunCMake/message/message-checks.cmake
new file mode 100644
index 0000000..605846e
--- /dev/null
+++ b/Tests/RunCMake/message/message-checks.cmake
@@ -0,0 +1,13 @@
+message(CHECK_START "Find `libfoo`")
+message(CHECK_START "Looking for `libfoo.h`")
+message(CHECK_PASS "found [/usr/include]")
+message(CHECK_START "Looking for `libfoo.so`")
+message(CHECK_PASS "found [/usr/lib/libfoo.so]")
+message(CHECK_START "Getting `libfoo` version")
+message(CHECK_START "Looking for `libfoo/version.h`")
+message(CHECK_PASS "found")
+message(CHECK_PASS "1.2.3")
+message(CHECK_FAIL "required version 4.5.6 but found 1.2.3")
+
+# Should generate an error, no associated CHECK_START
+message(CHECK_FAIL "unmatched check fail case")
-- 
cgit v0.12