CMake/Source/CTest/cmCTestScriptHandler.cxx
2025-03-20 16:31:49 +01:00

354 lines
12 KiB
C++

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmCTestScriptHandler.h"
#include <chrono>
#include <cstdlib>
#include <map>
#include <ratio>
#include <sstream>
#include <utility>
#include <cm/memory>
#include <cm3p/uv.h>
#include "cmCTest.h"
#include "cmCTestBuildCommand.h"
#include "cmCTestConfigureCommand.h"
#include "cmCTestCoverageCommand.h"
#include "cmCTestEmptyBinaryDirectoryCommand.h"
#include "cmCTestMemCheckCommand.h"
#include "cmCTestReadCustomFilesCommand.h"
#include "cmCTestRunScriptCommand.h"
#include "cmCTestSleepCommand.h"
#include "cmCTestStartCommand.h"
#include "cmCTestSubmitCommand.h"
#include "cmCTestTestCommand.h"
#include "cmCTestUpdateCommand.h"
#include "cmCTestUploadCommand.h"
#include "cmDuration.h"
#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmState.h"
#include "cmStateDirectory.h"
#include "cmStateSnapshot.h"
#include "cmSystemTools.h"
#include "cmUVHandlePtr.h"
#include "cmUVProcessChain.h"
#include "cmake.h"
cmCTestScriptHandler::cmCTestScriptHandler(cmCTest* ctest)
: CTest(ctest)
{
}
cmCTestScriptHandler::~cmCTestScriptHandler() = default;
// just adds an argument to the vector
void cmCTestScriptHandler::AddConfigurationScript(std::string const& script,
bool pscope)
{
this->ConfigurationScripts.emplace_back(script);
this->ScriptProcessScope.push_back(pscope);
}
// the generic entry point for handling scripts, this routine will run all
// the scripts provides a -S arguments
int cmCTestScriptHandler::ProcessHandler()
{
int res = 0;
for (size_t i = 0; i < this->ConfigurationScripts.size(); ++i) {
// for each script run it
res |= this->RunConfigurationScript(this->ConfigurationScripts[i],
this->ScriptProcessScope[i]);
}
return res;
}
void cmCTestScriptHandler::UpdateElapsedTime()
{
if (this->Makefile) {
// set the current elapsed time
auto itime = cmDurationTo<unsigned int>(this->CTest->GetElapsedTime());
auto timeString = std::to_string(itime);
this->Makefile->AddDefinition("CTEST_ELAPSED_TIME", timeString);
}
}
int cmCTestScriptHandler::ExecuteScript(std::string const& total_script_arg)
{
// execute the script passing in the arguments to the script as well as the
// arguments from this invocation of cmake
std::vector<std::string> argv;
argv.push_back(cmSystemTools::GetCTestCommand());
argv.push_back("-SR");
argv.push_back(total_script_arg);
cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
"Executable for CTest is: " << cmSystemTools::GetCTestCommand()
<< "\n");
// now pass through all the other arguments
std::vector<std::string>& initArgs =
this->CTest->GetInitialCommandLineArguments();
//*** need to make sure this does not have the current script ***
for (size_t i = 1; i < initArgs.size(); ++i) {
// in a nested subprocess, skip the parent's `-SR <path>` arguments.
if (initArgs[i] == "-SR") {
i++; // <path>
} else {
argv.push_back(initArgs[i]);
}
}
// Now create process object
cmUVProcessChainBuilder builder;
builder.AddCommand(argv)
.SetBuiltinStream(cmUVProcessChainBuilder::Stream_OUTPUT)
.SetBuiltinStream(cmUVProcessChainBuilder::Stream_ERROR);
auto process = builder.Start();
cm::uv_pipe_ptr outPipe;
outPipe.init(process.GetLoop(), 0);
uv_pipe_open(outPipe, process.OutputStream());
cm::uv_pipe_ptr errPipe;
errPipe.init(process.GetLoop(), 0);
uv_pipe_open(errPipe, process.ErrorStream());
std::vector<char> out;
std::vector<char> err;
std::string line;
auto pipe =
cmSystemTools::WaitForLine(&process.GetLoop(), outPipe, errPipe, line,
std::chrono::seconds(100), out, err);
while (pipe != cmSystemTools::WaitForLineResult::None) {
cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
"Output: " << line << "\n");
if (pipe == cmSystemTools::WaitForLineResult::STDERR) {
cmCTestLog(this->CTest, ERROR_MESSAGE, line << "\n");
} else if (pipe == cmSystemTools::WaitForLineResult::STDOUT) {
cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, line << "\n");
}
pipe =
cmSystemTools::WaitForLine(&process.GetLoop(), outPipe, errPipe, line,
std::chrono::seconds(100), out, err);
}
// Properly handle output of the build command
process.Wait();
auto const& status = process.GetStatus(0);
auto result = status.GetException();
int retVal = 0;
bool failed = false;
switch (result.first) {
case cmUVProcessChain::ExceptionCode::None:
retVal = static_cast<int>(status.ExitStatus);
break;
case cmUVProcessChain::ExceptionCode::Spawn:
cmCTestLog(this->CTest, ERROR_MESSAGE,
"\tError executing ctest: " << result.second << std::endl);
failed = true;
break;
default:
retVal = status.TermSignal;
cmCTestLog(this->CTest, ERROR_MESSAGE,
"\tThere was an exception: " << result.second << " " << retVal
<< std::endl);
failed = true;
}
if (failed) {
std::ostringstream message;
message << "Error running command: [";
message << static_cast<int>(result.first) << "] ";
for (std::string const& arg : argv) {
message << arg << " ";
}
cmCTestLog(this->CTest, ERROR_MESSAGE, message.str() << std::endl);
return -1;
}
return retVal;
}
void cmCTestScriptHandler::CreateCMake()
{
// create a cmake instance to read the configuration script
this->CMake = cm::make_unique<cmake>(cmake::RoleScript, cmState::CTest);
this->CMake->SetHomeDirectory("");
this->CMake->SetHomeOutputDirectory("");
this->CMake->GetCurrentSnapshot().SetDefaultDefinitions();
this->CMake->AddCMakePaths();
this->CMake->SetWorkingMode(cmake::SCRIPT_MODE,
cmake::CommandFailureAction::EXIT_CODE);
this->GlobalGenerator =
cm::make_unique<cmGlobalGenerator>(this->CMake.get());
cmStateSnapshot snapshot = this->CMake->GetCurrentSnapshot();
std::string cwd = cmSystemTools::GetLogicalWorkingDirectory();
snapshot.GetDirectory().SetCurrentSource(cwd);
snapshot.GetDirectory().SetCurrentBinary(cwd);
this->Makefile =
cm::make_unique<cmMakefile>(this->GlobalGenerator.get(), snapshot);
if (this->ParentMakefile) {
this->Makefile->SetRecursionDepth(
this->ParentMakefile->GetRecursionDepth());
}
this->CMake->SetProgressCallback(
[this](std::string const& m, float /*unused*/) {
if (!m.empty()) {
cmCTestLog(this->CTest, HANDLER_OUTPUT, "-- " << m << std::endl);
}
});
cmState* state = this->CMake->GetState();
state->AddBuiltinCommand("ctest_build", cmCTestBuildCommand(this->CTest));
state->AddBuiltinCommand("ctest_configure",
cmCTestConfigureCommand(this->CTest));
state->AddBuiltinCommand("ctest_coverage",
cmCTestCoverageCommand(this->CTest));
state->AddBuiltinCommand("ctest_empty_binary_directory",
cmCTestEmptyBinaryDirectoryCommand);
state->AddBuiltinCommand("ctest_memcheck",
cmCTestMemCheckCommand(this->CTest));
state->AddBuiltinCommand("ctest_read_custom_files",
cmCTestReadCustomFilesCommand(this->CTest));
state->AddBuiltinCommand("ctest_run_script",
cmCTestRunScriptCommand(this->CTest));
state->AddBuiltinCommand("ctest_sleep", cmCTestSleepCommand);
state->AddBuiltinCommand("ctest_start", cmCTestStartCommand(this->CTest));
state->AddBuiltinCommand("ctest_submit", cmCTestSubmitCommand(this->CTest));
state->AddBuiltinCommand("ctest_test", cmCTestTestCommand(this->CTest));
state->AddBuiltinCommand("ctest_update", cmCTestUpdateCommand(this->CTest));
state->AddBuiltinCommand("ctest_upload", cmCTestUploadCommand(this->CTest));
}
// this sets up some variables for the script to use, creates the required
// cmake instance and generators, and then reads in the script
int cmCTestScriptHandler::ReadInScript(std::string const& total_script_arg)
{
// Reset the error flag so that the script is read in no matter what
cmSystemTools::ResetErrorOccurredFlag();
// if the argument has a , in it then it needs to be broken into the fist
// argument (which is the script) and the second argument which will be
// passed into the scripts as S_ARG
std::string script;
std::string script_arg;
std::string::size_type const comma_pos = total_script_arg.find(',');
if (comma_pos != std::string::npos) {
script = total_script_arg.substr(0, comma_pos);
script_arg = total_script_arg.substr(comma_pos + 1);
} else {
script = total_script_arg;
}
// make sure the file exists
if (!cmSystemTools::FileExists(script)) {
cmSystemTools::Error("Cannot find file: " + script);
return 1;
}
// read in the list file to fill the cache
// create a cmake instance to read the configuration script
this->CreateCMake();
// set a variable with the path to the current script
this->Makefile->AddDefinition("CTEST_SCRIPT_DIRECTORY",
cmSystemTools::GetFilenamePath(script));
this->Makefile->AddDefinition("CTEST_SCRIPT_NAME",
cmSystemTools::GetFilenameName(script));
this->Makefile->AddDefinition("CTEST_EXECUTABLE_NAME",
cmSystemTools::GetCTestCommand());
this->Makefile->AddDefinition("CMAKE_EXECUTABLE_NAME",
cmSystemTools::GetCMakeCommand());
this->UpdateElapsedTime();
// set the CTEST_CONFIGURATION_TYPE variable to the current value of the
// the -C argument on the command line.
if (!this->CTest->GetConfigType().empty()) {
this->Makefile->AddDefinition("CTEST_CONFIGURATION_TYPE",
this->CTest->GetConfigType());
}
// add the script arg if defined
if (!script_arg.empty()) {
this->Makefile->AddDefinition("CTEST_SCRIPT_ARG", script_arg);
}
// set a callback function to update the elapsed time
this->Makefile->OnExecuteCommand([this] { this->UpdateElapsedTime(); });
/* Execute CTestScriptMode.cmake, which loads CMakeDetermineSystem and
CMakeSystemSpecificInformation, so
that variables like CMAKE_SYSTEM and also the search paths for libraries,
header and executables are set correctly and can be used. Makes new-style
ctest scripting easier. */
std::string systemFile =
this->Makefile->GetModulesFile("CTestScriptMode.cmake");
if (!this->Makefile->ReadListFile(systemFile) ||
cmSystemTools::GetErrorOccurredFlag()) {
cmCTestLog(this->CTest, ERROR_MESSAGE,
"Error in read:" << systemFile << "\n");
return -1;
}
// Add definitions of variables passed in on the command line:
std::map<std::string, std::string> const& defs =
this->CTest->GetDefinitions();
for (auto const& d : defs) {
this->Makefile->AddDefinition(d.first, d.second);
}
int res = 0;
// finally read in the script
if (!this->Makefile->ReadListFile(script) ||
cmSystemTools::GetErrorOccurredFlag()) {
// Reset the error flag so that it can run more than
// one script with an error when you use ctest_run_script.
cmSystemTools::ResetErrorOccurredFlag();
res = -1;
}
return this->CMake->HasScriptModeExitCode()
? this->CMake->GetScriptModeExitCode()
: res;
}
// run a specific script
int cmCTestScriptHandler::RunConfigurationScript(
std::string const& total_script_arg, bool pscope)
{
#ifndef CMAKE_BOOTSTRAP
cmSystemTools::SaveRestoreEnvironment sre;
#endif
int result;
// read in the script
if (pscope) {
cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
"Reading Script: " << total_script_arg << std::endl);
result = this->ReadInScript(total_script_arg);
} else {
cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
"Executing Script: " << total_script_arg << std::endl);
result = this->ExecuteScript(total_script_arg);
}
return result;
}
bool cmCTestScriptHandler::RunScript(cmCTest* ctest, cmMakefile* mf,
std::string const& sname, bool InProcess,
int* returnValue)
{
auto sh = cm::make_unique<cmCTestScriptHandler>(ctest);
sh->ParentMakefile = mf;
sh->AddConfigurationScript(sname, InProcess);
int res = sh->ProcessHandler();
if (returnValue) {
*returnValue = res;
}
return true;
}