blob: 2ad661cf043da254fd2f7ebcd56e9b8101617189 [file] [log] [blame]
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmCTestBuildAndTestHandler.h"
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <ratio>
#include "cmsys/Process.h"
#include "cmCTest.h"
#include "cmCTestTestHandler.h"
#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmState.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmWorkingDirectory.h"
#include "cmake.h"
cmCTestBuildAndTestHandler::cmCTestBuildAndTestHandler()
{
this->BuildTwoConfig = false;
this->BuildNoClean = false;
this->BuildNoCMake = false;
this->Timeout = cmDuration::zero();
}
void cmCTestBuildAndTestHandler::Initialize()
{
this->BuildTargets.clear();
this->Superclass::Initialize();
}
const char* cmCTestBuildAndTestHandler::GetOutput()
{
return this->Output.c_str();
}
int cmCTestBuildAndTestHandler::ProcessHandler()
{
this->Output.clear();
std::string output;
cmSystemTools::ResetErrorOccuredFlag();
int retv = this->RunCMakeAndTest(&this->Output);
cmSystemTools::ResetErrorOccuredFlag();
return retv;
}
int cmCTestBuildAndTestHandler::RunCMake(std::string* outstring,
std::ostringstream& out,
std::string& cmakeOutString,
cmake* cm)
{
std::vector<std::string> args;
args.push_back(cmSystemTools::GetCMakeCommand());
args.push_back(this->SourceDir);
if (!this->BuildGenerator.empty()) {
args.push_back("-G" + this->BuildGenerator);
}
if (!this->BuildGeneratorPlatform.empty()) {
args.push_back("-A" + this->BuildGeneratorPlatform);
}
if (!this->BuildGeneratorToolset.empty()) {
args.push_back("-T" + this->BuildGeneratorToolset);
}
const char* config = nullptr;
if (!this->CTest->GetConfigType().empty()) {
config = this->CTest->GetConfigType().c_str();
}
#ifdef CMAKE_INTDIR
if (!config) {
config = CMAKE_INTDIR;
}
#endif
if (config) {
args.push_back("-DCMAKE_BUILD_TYPE:STRING=" + std::string(config));
}
if (!this->BuildMakeProgram.empty() &&
(this->BuildGenerator.find("Make") != std::string::npos ||
this->BuildGenerator.find("Ninja") != std::string::npos)) {
args.push_back("-DCMAKE_MAKE_PROGRAM:FILEPATH=" + this->BuildMakeProgram);
}
for (std::string const& opt : this->BuildOptions) {
args.push_back(opt);
}
if (cm->Run(args) != 0) {
out << "Error: cmake execution failed\n";
out << cmakeOutString << "\n";
if (outstring) {
*outstring = out.str();
} else {
cmCTestLog(this->CTest, ERROR_MESSAGE, out.str() << std::endl);
}
return 1;
}
// do another config?
if (this->BuildTwoConfig) {
if (cm->Run(args) != 0) {
out << "Error: cmake execution failed\n";
out << cmakeOutString << "\n";
if (outstring) {
*outstring = out.str();
} else {
cmCTestLog(this->CTest, ERROR_MESSAGE, out.str() << std::endl);
}
return 1;
}
}
out << "======== CMake output ======\n";
out << cmakeOutString;
out << "======== End CMake output ======\n";
return 0;
}
class cmCTestBuildAndTestCaptureRAII
{
cmake& CM;
public:
cmCTestBuildAndTestCaptureRAII(cmake& cm, std::string& s)
: CM(cm)
{
cmSystemTools::SetMessageCallback(
[&s](const std::string& msg, const char* /*unused*/) {
s += msg;
s += "\n";
});
cmSystemTools::SetStdoutCallback([&s](std::string const& m) { s += m; });
cmSystemTools::SetStderrCallback([&s](std::string const& m) { s += m; });
this->CM.SetProgressCallback([&s](const std::string& msg, float prog) {
if (prog < 0) {
s += msg;
s += "\n";
}
});
}
~cmCTestBuildAndTestCaptureRAII()
{
this->CM.SetProgressCallback(nullptr);
cmSystemTools::SetStderrCallback(nullptr);
cmSystemTools::SetStdoutCallback(nullptr);
cmSystemTools::SetMessageCallback(nullptr);
}
cmCTestBuildAndTestCaptureRAII(const cmCTestBuildAndTestCaptureRAII&) =
delete;
cmCTestBuildAndTestCaptureRAII& operator=(
const cmCTestBuildAndTestCaptureRAII&) = delete;
};
int cmCTestBuildAndTestHandler::RunCMakeAndTest(std::string* outstring)
{
// if the generator and make program are not specified then it is an error
if (this->BuildGenerator.empty()) {
if (outstring) {
*outstring = "--build-and-test requires that the generator "
"be provided using the --build-generator "
"command line option. ";
}
return 1;
}
cmake cm(cmake::RoleProject, cmState::Project);
cm.SetHomeDirectory("");
cm.SetHomeOutputDirectory("");
std::string cmakeOutString;
cmCTestBuildAndTestCaptureRAII captureRAII(cm, cmakeOutString);
static_cast<void>(captureRAII);
std::ostringstream out;
if (this->CTest->GetConfigType().empty() && !this->ConfigSample.empty()) {
// use the config sample to set the ConfigType
std::string fullPath;
std::string resultingConfig;
std::vector<std::string> extraPaths;
std::vector<std::string> failed;
fullPath = cmCTestTestHandler::FindExecutable(
this->CTest, this->ConfigSample.c_str(), resultingConfig, extraPaths,
failed);
if (!fullPath.empty() && !resultingConfig.empty()) {
this->CTest->SetConfigType(resultingConfig.c_str());
}
out << "Using config sample with results: " << fullPath << " and "
<< resultingConfig << std::endl;
}
// we need to honor the timeout specified, the timeout include cmake, build
// and test time
auto clock_start = std::chrono::steady_clock::now();
// make sure the binary dir is there
out << "Internal cmake changing into directory: " << this->BinaryDir
<< std::endl;
if (!cmSystemTools::FileIsDirectory(this->BinaryDir)) {
cmSystemTools::MakeDirectory(this->BinaryDir);
}
cmWorkingDirectory workdir(this->BinaryDir);
if (workdir.Failed()) {
auto msg = "Failed to change working directory to " + this->BinaryDir +
" : " + std::strerror(workdir.GetLastResult()) + "\n";
if (outstring) {
*outstring = msg;
} else {
cmCTestLog(this->CTest, ERROR_MESSAGE, msg);
}
return 1;
}
if (this->BuildNoCMake) {
// Make the generator available for the Build call below.
cmGlobalGenerator* gen = cm.CreateGlobalGenerator(this->BuildGenerator);
cm.SetGlobalGenerator(gen);
if (!this->BuildGeneratorPlatform.empty()) {
cmMakefile mf(gen, cm.GetCurrentSnapshot());
if (!gen->SetGeneratorPlatform(this->BuildGeneratorPlatform, &mf)) {
return 1;
}
}
// Load the cache to make CMAKE_MAKE_PROGRAM available.
cm.LoadCache(this->BinaryDir);
} else {
// do the cmake step, no timeout here since it is not a sub process
if (this->RunCMake(outstring, out, cmakeOutString, &cm)) {
return 1;
}
}
// do the build
if (this->BuildTargets.empty()) {
this->BuildTargets.emplace_back();
}
for (std::string const& tar : this->BuildTargets) {
cmDuration remainingTime = std::chrono::seconds(0);
if (this->Timeout > cmDuration::zero()) {
remainingTime =
this->Timeout - (std::chrono::steady_clock::now() - clock_start);
if (remainingTime <= std::chrono::seconds(0)) {
if (outstring) {
*outstring = "--build-and-test timeout exceeded. ";
}
return 1;
}
}
std::string output;
const char* config = nullptr;
if (!this->CTest->GetConfigType().empty()) {
config = this->CTest->GetConfigType().c_str();
}
#ifdef CMAKE_INTDIR
if (!config) {
config = CMAKE_INTDIR;
}
#endif
if (!config) {
config = "Debug";
}
int retVal = cm.GetGlobalGenerator()->Build(
cmake::NO_BUILD_PARALLEL_LEVEL, this->SourceDir, this->BinaryDir,
this->BuildProject, { tar }, output, this->BuildMakeProgram, config,
!this->BuildNoClean, false, false, remainingTime);
out << output;
// if the build failed then return
if (retVal) {
if (outstring) {
*outstring = out.str();
}
return 1;
}
}
if (outstring) {
*outstring = out.str();
}
// if no test was specified then we are done
if (this->TestCommand.empty()) {
return 0;
}
// now run the compiled test if we can find it
// store the final location in fullPath
std::string fullPath;
std::string resultingConfig;
std::vector<std::string> extraPaths;
// if this->ExecutableDirectory is set try that as well
if (!this->ExecutableDirectory.empty()) {
std::string tempPath =
cmStrCat(this->ExecutableDirectory, '/', this->TestCommand);
extraPaths.push_back(tempPath);
}
std::vector<std::string> failed;
fullPath =
cmCTestTestHandler::FindExecutable(this->CTest, this->TestCommand.c_str(),
resultingConfig, extraPaths, failed);
if (!cmSystemTools::FileExists(fullPath)) {
out << "Could not find path to executable, perhaps it was not built: "
<< this->TestCommand << "\n";
out << "tried to find it in these places:\n";
out << fullPath << "\n";
for (std::string const& fail : failed) {
out << fail << "\n";
}
if (outstring) {
*outstring = out.str();
} else {
cmCTestLog(this->CTest, ERROR_MESSAGE, out.str());
}
return 1;
}
std::vector<const char*> testCommand;
testCommand.push_back(fullPath.c_str());
for (std::string const& testCommandArg : this->TestCommandArgs) {
testCommand.push_back(testCommandArg.c_str());
}
testCommand.push_back(nullptr);
std::string outs;
int retval = 0;
// run the test from the this->BuildRunDir if set
if (!this->BuildRunDir.empty()) {
out << "Run test in directory: " << this->BuildRunDir << "\n";
if (!workdir.SetDirectory(this->BuildRunDir)) {
out << "Failed to change working directory : "
<< std::strerror(workdir.GetLastResult()) << "\n";
if (outstring) {
*outstring = out.str();
} else {
cmCTestLog(this->CTest, ERROR_MESSAGE, out.str());
}
return 1;
}
}
out << "Running test command: \"" << fullPath << "\"";
for (std::string const& testCommandArg : this->TestCommandArgs) {
out << " \"" << testCommandArg << "\"";
}
out << "\n";
// how much time is remaining
cmDuration remainingTime = std::chrono::seconds(0);
if (this->Timeout > cmDuration::zero()) {
remainingTime =
this->Timeout - (std::chrono::steady_clock::now() - clock_start);
if (remainingTime <= std::chrono::seconds(0)) {
if (outstring) {
*outstring = "--build-and-test timeout exceeded. ";
}
return 1;
}
}
int runTestRes = this->CTest->RunTest(testCommand, &outs, &retval, nullptr,
remainingTime, nullptr);
if (runTestRes != cmsysProcess_State_Exited || retval != 0) {
out << "Test command failed: " << testCommand[0] << "\n";
retval = 1;
}
out << outs << "\n";
if (outstring) {
*outstring = out.str();
} else {
cmCTestLog(this->CTest, OUTPUT, out.str() << std::endl);
}
return retval;
}
int cmCTestBuildAndTestHandler::ProcessCommandLineArguments(
const std::string& currentArg, size_t& idx,
const std::vector<std::string>& allArgs)
{
// --build-and-test options
if (currentArg.find("--build-and-test", 0) == 0 &&
idx < allArgs.size() - 1) {
if (idx + 2 < allArgs.size()) {
idx++;
this->SourceDir = allArgs[idx];
idx++;
this->BinaryDir = allArgs[idx];
// dir must exist before CollapseFullPath is called
cmSystemTools::MakeDirectory(this->BinaryDir);
this->BinaryDir = cmSystemTools::CollapseFullPath(this->BinaryDir);
this->SourceDir = cmSystemTools::CollapseFullPath(this->SourceDir);
} else {
cmCTestLog(this->CTest, ERROR_MESSAGE,
"--build-and-test must have source and binary dir"
<< std::endl);
return 0;
}
}
if (currentArg.find("--build-target", 0) == 0 && idx < allArgs.size() - 1) {
idx++;
this->BuildTargets.push_back(allArgs[idx]);
}
if (currentArg.find("--build-nocmake", 0) == 0) {
this->BuildNoCMake = true;
}
if (currentArg.find("--build-run-dir", 0) == 0 && idx < allArgs.size() - 1) {
idx++;
this->BuildRunDir = allArgs[idx];
}
if (currentArg.find("--build-two-config", 0) == 0) {
this->BuildTwoConfig = true;
}
if (currentArg.find("--build-exe-dir", 0) == 0 && idx < allArgs.size() - 1) {
idx++;
this->ExecutableDirectory = allArgs[idx];
}
if (currentArg.find("--test-timeout", 0) == 0 && idx < allArgs.size() - 1) {
idx++;
this->Timeout = cmDuration(atof(allArgs[idx].c_str()));
}
if (currentArg == "--build-generator" && idx < allArgs.size() - 1) {
idx++;
this->BuildGenerator = allArgs[idx];
}
if (currentArg == "--build-generator-platform" && idx < allArgs.size() - 1) {
idx++;
this->BuildGeneratorPlatform = allArgs[idx];
}
if (currentArg == "--build-generator-toolset" && idx < allArgs.size() - 1) {
idx++;
this->BuildGeneratorToolset = allArgs[idx];
}
if (currentArg.find("--build-project", 0) == 0 && idx < allArgs.size() - 1) {
idx++;
this->BuildProject = allArgs[idx];
}
if (currentArg.find("--build-makeprogram", 0) == 0 &&
idx < allArgs.size() - 1) {
idx++;
this->BuildMakeProgram = allArgs[idx];
}
if (currentArg.find("--build-config-sample", 0) == 0 &&
idx < allArgs.size() - 1) {
idx++;
this->ConfigSample = allArgs[idx];
}
if (currentArg.find("--build-noclean", 0) == 0) {
this->BuildNoClean = true;
}
if (currentArg.find("--build-options", 0) == 0) {
while (idx + 1 < allArgs.size() && allArgs[idx + 1] != "--build-target" &&
allArgs[idx + 1] != "--test-command") {
++idx;
this->BuildOptions.push_back(allArgs[idx]);
}
}
if (currentArg.find("--test-command", 0) == 0 && idx < allArgs.size() - 1) {
++idx;
this->TestCommand = allArgs[idx];
while (idx + 1 < allArgs.size()) {
++idx;
this->TestCommandArgs.push_back(allArgs[idx]);
}
}
return 1;
}