ctest: Improve colorization in output

Refactor CTest's logging to replace custom color code handling with the
infrastructure introduced in commit 329d755dbd (StdIo: Add a Terminal
abstraction to print color text, 2025-05-06).

Avoid ANSI escape sequences in files passed to `--output-log-file`.

Fixes: #27915
This commit is contained in:
Tyler Yankee
2026-07-03 10:02:43 -04:00
parent db58e5fbd6
commit fd395b25b5
8 changed files with 114 additions and 79 deletions
+22 -31
View File
@@ -52,6 +52,7 @@
#include "cmMakefile.h"
#include "cmState.h"
#include "cmStateSnapshot.h"
#include "cmStdIoTerminal.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmTestDiscovery.h"
@@ -655,27 +656,21 @@ void cmCTestTestHandler::LogTestSummary(std::vector<std::string> const& passed,
percent = 99;
}
std::string passColorCode;
std::string failedColorCode;
cm::StdIo::TermAttrSet summaryAttrs;
if (failed.empty()) {
passColorCode = this->CTest->GetColorCode(cmCTest::Color::GREEN);
summaryAttrs = cm::StdIo::TermAttr::ForegroundGreen;
} else {
failedColorCode = this->CTest->GetColorCode(cmCTest::Color::RED);
summaryAttrs = cm::StdIo::TermAttr::ForegroundRed;
}
if (failed.empty()) {
cmCTestLog(this->CTest, HANDLER_OUTPUT,
std::endl
<< passColorCode << std::lround(percent) << "% tests passed"
<< this->CTest->GetColorCode(cmCTest::Color::CLEAR_COLOR)
<< " out of " << total << std::endl);
cmCTestColorLog(this->CTest, HANDLER_OUTPUT, summaryAttrs,
cmStrCat("\n", std::lround(percent),
"% tests passed out of ", total, "\n"));
} else {
cmCTestLog(this->CTest, HANDLER_OUTPUT,
std::endl
<< passColorCode << std::lround(percent) << "% tests passed"
<< this->CTest->GetColorCode(cmCTest::Color::CLEAR_COLOR)
<< ", " << failedColorCode << failed.size() << " tests failed"
<< this->CTest->GetColorCode(cmCTest::Color::CLEAR_COLOR)
<< " out of " << total << std::endl);
cmCTestColorLog(this->CTest, HANDLER_OUTPUT, summaryAttrs,
cmStrCat("\n", std::lround(percent), "% tests passed, ",
failed.size(), " tests failed out of ", total,
"\n"));
}
if ((!this->CTest->GetLabelsForSubprojects().empty() &&
this->CTest->GetSubprojectSummary())) {
@@ -702,8 +697,7 @@ void cmCTestTestHandler::LogDisabledTests(
this->StartLogFile("TestsDisabled", ofs);
char const* disabled_reason;
cmCTestLog(this->CTest, HANDLER_OUTPUT,
this->CTest->GetColorCode(cmCTest::Color::BLUE));
cm::StdIo::TermAttrSet disabledAttrs = cm::StdIo::TermAttr::ForegroundBlue;
for (cmCTestTestResult const& dt : disabledTests) {
ofs << dt.TestCount << ":" << dt.Name << std::endl;
if (dt.CompletionStatus == "Disabled") {
@@ -711,12 +705,11 @@ void cmCTestTestHandler::LogDisabledTests(
} else {
disabled_reason = "Skipped";
}
cmCTestLog(this->CTest, HANDLER_OUTPUT,
"\t" << std::setw(3) << dt.TestCount << " - " << dt.Name
<< " (" << disabled_reason << ")" << std::endl);
std::ostringstream msg;
msg << "\t" << std::setw(3) << dt.TestCount << " - " << dt.Name << " ("
<< disabled_reason << ")\n";
cmCTestColorLog(this->CTest, HANDLER_OUTPUT, disabledAttrs, msg.str());
}
cmCTestLog(this->CTest, HANDLER_OUTPUT,
this->CTest->GetColorCode(cmCTest::Color::CLEAR_COLOR));
}
}
@@ -735,9 +728,9 @@ void cmCTestTestHandler::LogFailedTests(std::vector<std::string> const& failed,
!cmHasLiteralPrefix(ft.CompletionStatus, "SKIP_") &&
ft.CompletionStatus != "Disabled") {
ofs << ft.TestCount << ":" << ft.Name << std::endl;
auto testColor = cmCTest::Color::RED;
cm::StdIo::TermAttrSet testAttrs = cm::StdIo::TermAttr::ForegroundRed;
if (this->GetTestStatus(ft) == "Not Run") {
testColor = cmCTest::Color::YELLOW;
testAttrs = cm::StdIo::TermAttr::ForegroundYellow;
}
std::string ft_name_and_status =
cmStrCat(ft.Name, " (", this->GetTestStatus(ft), ')');
@@ -750,12 +743,10 @@ void cmCTestTestHandler::LogFailedTests(std::vector<std::string> const& failed,
: maxLen - ft_name_and_status.size();
labels = cmStrCat(std::string(ns, ' '), cmJoin(p.Labels, " "));
}
cmCTestLog(
this->CTest, HANDLER_OUTPUT,
"\t" << this->CTest->GetColorCode(testColor) << std::setw(3)
<< ft.TestCount << " - " << ft_name_and_status
<< this->CTest->GetColorCode(cmCTest::Color::CLEAR_COLOR)
<< labels << std::endl);
std::ostringstream msg;
msg << "\t" << std::setw(3) << ft.TestCount << " - "
<< ft_name_and_status << labels << "\n";
cmCTestColorLog(this->CTest, HANDLER_OUTPUT, testAttrs, msg.str());
}
}
}
+22 -33
View File
@@ -11,7 +11,6 @@
#include <ctime>
#include <functional>
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <map>
#include <ratio>
@@ -63,6 +62,7 @@
#include "cmStateSnapshot.h"
#include "cmStateTypes.h"
#include "cmStdIoStream.h"
#include "cmStdIoTerminal.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmUVHandlePtr.h"
@@ -197,7 +197,6 @@ struct cmCTest::Private
cm::optional<cmCTest::LogType> OutputLogFileLastTag;
bool OutputTestOutputOnTestFailure = false;
bool OutputColorCode = cmCTest::ColoredOutputSupportedByConsole();
std::map<std::string, std::string> Definitions;
@@ -1565,20 +1564,6 @@ bool cmCTest::ProgressOutputSupportedByConsole()
return cm::StdIo::Out().Kind() == cm::StdIo::TermKind::VT100;
}
bool cmCTest::ColoredOutputSupportedByConsole()
{
std::string clicolor_force;
if (cmSystemTools::GetEnv("CLICOLOR_FORCE", clicolor_force) &&
!clicolor_force.empty() && clicolor_force != "0") {
return true;
}
std::string clicolor;
if (cmSystemTools::GetEnv("CLICOLOR", clicolor) && clicolor == "0") {
return false;
}
return cm::StdIo::Out().Kind() == cm::StdIo::TermKind::VT100;
}
bool cmCTest::AddVariableDefinition(std::string const& arg)
{
std::string name;
@@ -3559,6 +3544,13 @@ static char const* cmCTestStringLogType[] = { "DEBUG",
"ERROR_MESSAGE" };
void cmCTest::Log(LogType logType, std::string msg, bool suppress)
{
static cm::StdIo::TermAttrSet const noAttrs;
this->Log(logType, std::move(msg), noAttrs, suppress);
}
void cmCTest::Log(LogType logType, std::string msg,
cm::StdIo::TermAttrSet const& attrs, bool suppress)
{
if (msg.empty()) {
return;
@@ -3598,7 +3590,7 @@ void cmCTest::Log(LogType logType, std::string msg, bool suppress)
if (this->Impl->TestProgressOutput) {
if (this->Impl->TestProgressNewlinePending) {
this->Impl->TestProgressNewlinePending = false;
std::cout << '\r';
cm::StdIo::Out().IOS() << '\r';
}
if (msg.find('\n') != std::string::npos) {
@@ -3608,7 +3600,7 @@ void cmCTest::Log(LogType logType, std::string msg, bool suppress)
// ProgressOutputSupportedByConsole() already verified VT100 support.
// Erase the rest of the line before printing the message.
std::cout << kVT100_EraseLine << msg << std::flush;
cm::StdIo::Out().IOS() << kVT100_EraseLine << msg << std::flush;
return;
}
logType = HANDLER_OUTPUT;
@@ -3617,42 +3609,39 @@ void cmCTest::Log(LogType logType, std::string msg, bool suppress)
switch (logType) {
case DEBUG:
if (this->Impl->Debug) {
std::cout << msg << std::flush;
cm::StdIo::Print(cm::StdIo::Out(), attrs, msg);
cm::StdIo::Out().IOS() << std::flush;
}
break;
case OUTPUT:
case HANDLER_OUTPUT:
if (this->Impl->Debug || this->Impl->Verbose) {
std::cout << msg << std::flush;
cm::StdIo::Print(cm::StdIo::Out(), attrs, msg);
cm::StdIo::Out().IOS() << std::flush;
}
break;
case HANDLER_VERBOSE_OUTPUT:
if (this->Impl->Debug || this->Impl->ExtraVerbose) {
std::cout << msg << std::flush;
cm::StdIo::Print(cm::StdIo::Out(), attrs, msg);
cm::StdIo::Out().IOS() << std::flush;
}
break;
case WARNING:
std::cerr << msg << std::flush;
cm::StdIo::Print(cm::StdIo::Err(), attrs, msg);
cm::StdIo::Err().IOS() << std::flush;
break;
case ERROR_MESSAGE:
std::cerr << msg << std::flush;
cm::StdIo::Print(cm::StdIo::Err(), attrs, msg);
cm::StdIo::Err().IOS() << std::flush;
cmSystemTools::SetErrorOccurred();
break;
default:
std::cout << msg << std::flush;
cm::StdIo::Print(cm::StdIo::Out(), attrs, msg);
cm::StdIo::Out().IOS() << std::flush;
}
}
}
std::string cmCTest::GetColorCode(Color color) const
{
if (this->Impl->OutputColorCode) {
return cmStrCat("\033[0;", static_cast<int>(color), 'm');
}
return {};
}
void cmCTest::SetTimeLimit(cmValue val)
{
this->Impl->TimeLimit =
+12 -15
View File
@@ -18,6 +18,7 @@
#include "cmDuration.h"
#include "cmProcessOutput.h"
#include "cmStdIoTerminal.h"
class cmake;
class cmCMakePresetsArgs;
@@ -346,18 +347,10 @@ public:
/** Add log to the output */
void Log(LogType logType, std::string msg, bool suppress = false);
/** Color values */
enum class Color
{
CLEAR_COLOR = 0,
RED = 31,
GREEN = 32,
YELLOW = 33,
BLUE = 34
};
/** Get color code characters for a specific color */
std::string GetColorCode(Color color) const;
/** Add log to the output with terminal attributes for console output only.
*/
void Log(LogType logType, std::string msg,
cm::StdIo::TermAttrSet const& attrs, bool suppress = false);
/** The Build ID is assigned by CDash */
void SetBuildID(std::string const& id);
@@ -464,9 +457,6 @@ private:
/** returns true iff the console supports progress output */
static bool ProgressOutputSupportedByConsole();
/** returns true iff the console supports colored output */
static bool ColoredOutputSupportedByConsole();
/** Create note from files. */
int GenerateCTestNotesOutput(cmXMLWriter& xml, cmake* cm,
std::vector<std::string> const& files);
@@ -496,3 +486,10 @@ private:
cmCTestLog_msg << msg; \
(ctSelf)->Log(cmCTest::logType, cmCTestLog_msg.str(), suppress); \
} while (false)
#define cmCTestColorLog(ctSelf, logType, attrs, msg) \
do { \
std::ostringstream cmCTestLog_msg; \
cmCTestLog_msg << msg; \
(ctSelf)->Log(cmCTest::logType, cmCTestLog_msg.str(), attrs); \
} while (false)
@@ -0,0 +1,38 @@
set(log "${RunCMake_TEST_BINARY_DIR}/output-log.txt")
if(NOT EXISTS "${log}")
set(RunCMake_TEST_FAILED "The expected output log file is missing:\n ${log}")
return()
endif()
file(READ "${log}" log_content)
string(ASCII 27 esc)
set(esc_re "${esc}\\[[0-9;]*m")
if(log_content MATCHES "${esc_re}")
set(RunCMake_TEST_FAILED
"output-log.txt contains ANSI color escape sequences from CTest output.")
endif()
if(NOT actual_stdout MATCHES "${esc_re}" AND NOT actual_stderr MATCHES "${esc_re}")
set(RunCMake_TEST_FAILED
"Neither stdout nor stderr contains ANSI color escape sequences with CLICOLOR_FORCE=1.")
return()
endif()
if(NOT actual_stdout MATCHES
"${esc_re}[\n][0-9]+% tests passed, [0-9]+ tests failed out of [0-9]+[\n]${esc_re}")
set(RunCMake_TEST_FAILED "stdout does not contain color-wrapped failed-summary line.")
endif()
if(actual_stdout MATCHES "${esc_re}The following tests FAILED:")
set(RunCMake_TEST_FAILED "The failed-tests header is unexpectedly colorized.")
endif()
if(NOT actual_stdout MATCHES
"The following tests FAILED:[\n]${esc_re}[^\n]*OutputLogNoEscape-Fail \\\(Failed\\\)")
set(RunCMake_TEST_FAILED "stdout does not contain color-wrapped failed-test entry.")
endif()
if(actual_stdout MATCHES "${esc_re}Total Test time \\\(real\\\)")
set(RunCMake_TEST_FAILED "The total test time line is unexpectedly colorized.")
endif()
@@ -0,0 +1 @@
8
@@ -0,0 +1 @@
Errors while running CTest
+7
View File
@@ -0,0 +1,7 @@
include(CTest)
add_test(NAME OutputLogNoEscape-Pass
COMMAND ${CMAKE_COMMAND} -E true)
add_test(NAME OutputLogNoEscape-Fail
COMMAND ${CMAKE_COMMAND} -E false)
+11
View File
@@ -40,6 +40,17 @@ if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
run_SingleConfig()
endif()
function(run_ColorOutput)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/ColorOutput-build)
run_cmake(ColorOutput)
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(ColorOutput-build ${CMAKE_COMMAND} --build . --config Debug)
run_cmake_command(ColorOutput-test
${CMAKE_COMMAND} -E env CLICOLOR_FORCE=1
${CMAKE_CTEST_COMMAND} -C Debug --output-log output-log.txt)
endfunction()
run_ColorOutput()
run_cmake(CMP0145-Dart-OLD)
run_cmake(CMP0145-Dart-WARN)
run_cmake(CMP0145-Dart-NEW)