/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmCTestRunTest.h"

#include <chrono>
#include <cstddef> // IWYU pragma: keep
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <iomanip>
#include <ratio>
#include <sstream>
#include <utility>

#include <cm/memory>

#include "cmsys/RegularExpression.hxx"

#include "cmCTest.h"
#include "cmCTestMemCheckHandler.h"
#include "cmCTestMultiProcessHandler.h"
#include "cmProcess.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmWorkingDirectory.h"

cmCTestRunTest::cmCTestRunTest(cmCTestMultiProcessHandler& multiHandler)
  : MultiTestHandler(multiHandler)
{
  CTest = multiHandler.CTest;
  TestHandler = multiHandler.TestHandler;
  TestResult.ExecutionTime = cmDuration::zero();
  TestResult.ReturnValue = 0;
  TestResult.Status = cmCTestTestHandler::NOT_RUN;
  TestResult.TestCount = 0;
  TestResult.Properties = nullptr;
}

void cmCTestRunTest::CheckOutput(std::string const& line)
{
  cmCTestLog(CTest, HANDLER_VERBOSE_OUTPUT,
             GetIndex() << ": " << line << std::endl);
  ProcessOutput += line;
  ProcessOutput += "\n";

  // Check for TIMEOUT_AFTER_MATCH property.
  if (!TestProperties->TimeoutRegularExpressions.empty()) {
    for (auto& reg : TestProperties->TimeoutRegularExpressions) {
      if (reg.first.find(ProcessOutput)) {
        cmCTestLog(CTest, HANDLER_VERBOSE_OUTPUT,
                   GetIndex()
                     << ": "
                     << "Test timeout changed to "
                     << std::chrono::duration_cast<std::chrono::seconds>(
                          TestProperties->AlternateTimeout)
                          .count()
                     << std::endl);
        TestProcess->ResetStartTime();
        TestProcess->ChangeTimeout(TestProperties->AlternateTimeout);
        TestProperties->TimeoutRegularExpressions.clear();
        break;
      }
    }
  }
}

bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started)
{
  WriteLogOutputTop(completed, total);
  std::string reason;
  bool passed = true;
  cmProcess::State res =
    started ? TestProcess->GetProcessStatus() : cmProcess::State::Error;
  if (res != cmProcess::State::Expired) {
    TimeoutIsForStopTime = false;
  }
  std::int64_t retVal = TestProcess->GetExitValue();
  bool forceFail = false;
  bool forceSkip = false;
  bool skipped = false;
  bool outputTestErrorsToConsole = false;
  if (!TestProperties->RequiredRegularExpressions.empty() &&
      FailedDependencies.empty()) {
    bool found = false;
    for (auto& pass : TestProperties->RequiredRegularExpressions) {
      if (pass.first.find(ProcessOutput)) {
        found = true;
        reason = cmStrCat("Required regular expression found. Regex=[",
                          pass.second, ']');
        break;
      }
    }
    if (!found) {
      reason = "Required regular expression not found. Regex=[";
      for (auto& pass : TestProperties->RequiredRegularExpressions) {
        reason += pass.second;
        reason += "\n";
      }
      reason += "]";
      forceFail = true;
    }
  }
  if (!TestProperties->ErrorRegularExpressions.empty() &&
      FailedDependencies.empty()) {
    for (auto& fail : TestProperties->ErrorRegularExpressions) {
      if (fail.first.find(ProcessOutput)) {
        reason = cmStrCat("Error regular expression found in output. Regex=[",
                          fail.second, ']');
        forceFail = true;
        break;
      }
    }
  }
  if (!TestProperties->SkipRegularExpressions.empty() &&
      FailedDependencies.empty()) {
    for (auto& skip : TestProperties->SkipRegularExpressions) {
      if (skip.first.find(ProcessOutput)) {
        reason = cmStrCat("Skip regular expression found in output. Regex=[",
                          skip.second, ']');
        forceSkip = true;
        break;
      }
    }
  }
  std::ostringstream outputStream;
  if (res == cmProcess::State::Exited) {
    bool success = !forceFail &&
      (retVal == 0 || !TestProperties->RequiredRegularExpressions.empty());
    if ((TestProperties->SkipReturnCode >= 0 &&
         TestProperties->SkipReturnCode == retVal) ||
        forceSkip) {
      TestResult.Status = cmCTestTestHandler::NOT_RUN;
      std::ostringstream s;
      if (forceSkip) {
        s << "SKIP_REGULAR_EXPRESSION_MATCHED";
      } else {
        s << "SKIP_RETURN_CODE=" << TestProperties->SkipReturnCode;
      }
      TestResult.CompletionStatus = s.str();
      cmCTestLog(CTest, HANDLER_OUTPUT, "***Skipped ");
      skipped = true;
    } else if (success != TestProperties->WillFail) {
      TestResult.Status = cmCTestTestHandler::COMPLETED;
      outputStream << "   Passed  ";
    } else {
      TestResult.Status = cmCTestTestHandler::FAILED;
      outputStream << "***Failed  " << reason;
      outputTestErrorsToConsole = CTest->GetOutputTestOutputOnTestFailure();
    }
  } else if (res == cmProcess::State::Expired) {
    outputStream << "***Timeout ";
    TestResult.Status = cmCTestTestHandler::TIMEOUT;
    outputTestErrorsToConsole = CTest->GetOutputTestOutputOnTestFailure();
  } else if (res == cmProcess::State::Exception) {
    outputTestErrorsToConsole = CTest->GetOutputTestOutputOnTestFailure();
    outputStream << "***Exception: ";
    TestResult.ExceptionStatus = TestProcess->GetExitExceptionString();
    switch (TestProcess->GetExitException()) {
      case cmProcess::Exception::Fault:
        outputStream << "SegFault";
        TestResult.Status = cmCTestTestHandler::SEGFAULT;
        break;
      case cmProcess::Exception::Illegal:
        outputStream << "Illegal";
        TestResult.Status = cmCTestTestHandler::ILLEGAL;
        break;
      case cmProcess::Exception::Interrupt:
        outputStream << "Interrupt";
        TestResult.Status = cmCTestTestHandler::INTERRUPT;
        break;
      case cmProcess::Exception::Numerical:
        outputStream << "Numerical";
        TestResult.Status = cmCTestTestHandler::NUMERICAL;
        break;
      default:
        cmCTestLog(CTest, HANDLER_OUTPUT, TestResult.ExceptionStatus);
        TestResult.Status = cmCTestTestHandler::OTHER_FAULT;
    }
  } else if ("Disabled" == TestResult.CompletionStatus) {
    outputStream << "***Not Run (Disabled) ";
  } else // cmProcess::State::Error
  {
    outputStream << "***Not Run ";
  }

  passed = TestResult.Status == cmCTestTestHandler::COMPLETED;
  char buf[1024];
  sprintf(buf, "%6.2f sec", TestProcess->GetTotalTime().count());
  outputStream << buf << "\n";

  if (CTest->GetTestProgressOutput()) {
    if (!passed) {
      // If the test did not pass, reprint test name and error
      std::string output = GetTestPrefix(completed, total);
      std::string testName = TestProperties->Name;
      const int maxTestNameWidth = CTest->GetMaxTestNameWidth();
      testName.resize(maxTestNameWidth + 4, '.');

      output += testName;
      output += outputStream.str();
      outputStream.str("");
      outputStream.clear();
      outputStream << output;
      cmCTestLog(CTest, HANDLER_TEST_PROGRESS_OUTPUT, "\n"); // flush
    }
    if (completed == total) {
      std::string testName =
        GetTestPrefix(completed, total) + TestProperties->Name + "\n";
      cmCTestLog(CTest, HANDLER_TEST_PROGRESS_OUTPUT, testName);
    }
  }
  if (!CTest->GetTestProgressOutput() || !passed) {
    cmCTestLog(CTest, HANDLER_OUTPUT, outputStream.str());
  }

  if (outputTestErrorsToConsole) {
    cmCTestLog(CTest, HANDLER_OUTPUT, ProcessOutput << std::endl);
  }

  if (TestHandler->LogFile) {
    *TestHandler->LogFile << "Test time = " << buf << std::endl;
  }

  DartProcessing();

  // if this is doing MemCheck then all the output needs to be put into
  // Output since that is what is parsed by cmCTestMemCheckHandler
  if (!TestHandler->MemCheck && started) {
    TestHandler->CleanTestOutput(
      ProcessOutput,
      static_cast<size_t>(TestResult.Status == cmCTestTestHandler::COMPLETED
                            ? TestHandler->CustomMaximumPassedTestOutputSize
                            : TestHandler->CustomMaximumFailedTestOutputSize));
  }
  TestResult.Reason = reason;
  if (TestHandler->LogFile) {
    bool pass = true;
    const char* reasonType = "Test Pass Reason";
    if (TestResult.Status != cmCTestTestHandler::COMPLETED &&
        TestResult.Status != cmCTestTestHandler::NOT_RUN) {
      reasonType = "Test Fail Reason";
      pass = false;
    }
    auto ttime = TestProcess->GetTotalTime();
    auto hours = std::chrono::duration_cast<std::chrono::hours>(ttime);
    ttime -= hours;
    auto minutes = std::chrono::duration_cast<std::chrono::minutes>(ttime);
    ttime -= minutes;
    auto seconds = std::chrono::duration_cast<std::chrono::seconds>(ttime);
    char buffer[100];
    sprintf(buffer, "%02d:%02d:%02d", static_cast<unsigned>(hours.count()),
            static_cast<unsigned>(minutes.count()),
            static_cast<unsigned>(seconds.count()));
    *TestHandler->LogFile
      << "----------------------------------------------------------"
      << std::endl;
    if (!TestResult.Reason.empty()) {
      *TestHandler->LogFile << reasonType << ":\n"
                            << TestResult.Reason << "\n";
    } else {
      if (pass) {
        *TestHandler->LogFile << "Test Passed.\n";
      } else {
        *TestHandler->LogFile << "Test Failed.\n";
      }
    }
    *TestHandler->LogFile
      << "\"" << TestProperties->Name
      << "\" end time: " << CTest->CurrentTime() << std::endl
      << "\"" << TestProperties->Name << "\" time elapsed: " << buffer
      << std::endl
      << "----------------------------------------------------------"
      << std::endl
      << std::endl;
  }
  // if the test actually started and ran
  // record the results in TestResult
  if (started) {
    std::string compressedOutput;
    if (!TestHandler->MemCheck && CTest->ShouldCompressTestOutput()) {
      std::string str = ProcessOutput;
      if (CTest->CompressString(str)) {
        compressedOutput = std::move(str);
      }
    }
    bool compress = !compressedOutput.empty() &&
      compressedOutput.length() < ProcessOutput.length();
    TestResult.Output = compress ? compressedOutput : ProcessOutput;
    TestResult.CompressOutput = compress;
    TestResult.ReturnValue = TestProcess->GetExitValue();
    if (!skipped) {
      TestResult.CompletionStatus = "Completed";
    }
    TestResult.ExecutionTime = TestProcess->GetTotalTime();
    MemCheckPostProcess();
    ComputeWeightedCost();
  }
  // If the test does not need to rerun push the current TestResult onto the
  // TestHandler vector
  if (!NeedsToRepeat()) {
    TestHandler->TestResults.push_back(TestResult);
  }
  TestProcess.reset();
  return passed || skipped;
}

bool cmCTestRunTest::StartAgain(std::unique_ptr<cmCTestRunTest> runner,
                                size_t completed)
{
  auto* testRun = runner.get();

  if (!testRun->RunAgain) {
    return false;
  }
  testRun->RunAgain = false; // reset
  testRun->TestProcess = cm::make_unique<cmProcess>(std::move(runner));
  // change to tests directory
  cmWorkingDirectory workdir(testRun->TestProperties->Directory);
  if (workdir.Failed()) {
    testRun->StartFailure("Failed to change working directory to " +
                            testRun->TestProperties->Directory + " : " +
                            std::strerror(workdir.GetLastResult()),
                          "Failed to change working directory");
    return true;
  }

  testRun->StartTest(completed, testRun->TotalNumberOfTests);
  return true;
}

bool cmCTestRunTest::NeedsToRepeat()
{
  NumberOfRunsLeft--;
  if (NumberOfRunsLeft == 0) {
    return false;
  }
  // If a test is marked as NOT_RUN it will not be repeated
  // no matter the repeat settings, so just record it as-is.
  if (TestResult.Status == cmCTestTestHandler::NOT_RUN) {
    return false;
  }
  // if number of runs left is not 0, and we are running until
  // we find a failed (or passed) test, then return true so the test can be
  // restarted
  if ((RepeatMode == cmCTest::Repeat::UntilFail &&
       TestResult.Status == cmCTestTestHandler::COMPLETED) ||
      (RepeatMode == cmCTest::Repeat::UntilPass &&
       TestResult.Status != cmCTestTestHandler::COMPLETED) ||
      (RepeatMode == cmCTest::Repeat::AfterTimeout &&
       TestResult.Status == cmCTestTestHandler::TIMEOUT)) {
    RunAgain = true;
    return true;
  }
  return false;
}
void cmCTestRunTest::ComputeWeightedCost()
{
  double prev = static_cast<double>(TestProperties->PreviousRuns);
  double avgcost = static_cast<double>(TestProperties->Cost);
  double current = TestResult.ExecutionTime.count();

  if (TestResult.Status == cmCTestTestHandler::COMPLETED) {
    TestProperties->Cost =
      static_cast<float>(((prev * avgcost) + current) / (prev + 1.0));
    TestProperties->PreviousRuns++;
  }
}

void cmCTestRunTest::MemCheckPostProcess()
{
  if (!TestHandler->MemCheck) {
    return;
  }
  cmCTestOptionalLog(CTest, HANDLER_VERBOSE_OUTPUT,
                     Index << ": process test output now: "
                           << TestProperties->Name << " " << TestResult.Name
                           << std::endl,
                     TestHandler->GetQuiet());
  cmCTestMemCheckHandler* handler =
    static_cast<cmCTestMemCheckHandler*>(TestHandler);
  handler->PostProcessTest(TestResult, Index);
}

void cmCTestRunTest::StartFailure(std::unique_ptr<cmCTestRunTest> runner,
                                  std::string const& output,
                                  std::string const& detail)
{
  auto* testRun = runner.get();

  testRun->TestProcess = cm::make_unique<cmProcess>(std::move(runner));
  testRun->StartFailure(output, detail);

  testRun->FinalizeTest(false);
}

void cmCTestRunTest::StartFailure(std::string const& output,
                                  std::string const& detail)
{
  // Still need to log the Start message so the test summary records our
  // attempt to start this test
  if (!CTest->GetTestProgressOutput()) {
    cmCTestLog(
      CTest, HANDLER_OUTPUT,
      std::setw(2 * getNumWidth(TotalNumberOfTests) + 8)
        << "Start " << std::setw(getNumWidth(TestHandler->GetMaxIndex()))
        << TestProperties->Index << ": " << TestProperties->Name << std::endl);
  }

  ProcessOutput.clear();
  if (!output.empty()) {
    *TestHandler->LogFile << output << std::endl;
    cmCTestLog(CTest, ERROR_MESSAGE, output << std::endl);
  }

  TestResult.Properties = TestProperties;
  TestResult.ExecutionTime = cmDuration::zero();
  TestResult.CompressOutput = false;
  TestResult.ReturnValue = -1;
  TestResult.CompletionStatus = detail;
  TestResult.Status = cmCTestTestHandler::NOT_RUN;
  TestResult.TestCount = TestProperties->Index;
  TestResult.Name = TestProperties->Name;
  TestResult.Path = TestProperties->Directory;
  TestResult.Output = output;
  TestResult.FullCommandLine.clear();
  TestResult.Environment.clear();
}

std::string cmCTestRunTest::GetTestPrefix(size_t completed, size_t total) const
{
  std::ostringstream outputStream;
  outputStream << std::setw(getNumWidth(total)) << completed << "/";
  outputStream << std::setw(getNumWidth(total)) << total << " ";

  if (TestHandler->MemCheck) {
    outputStream << "MemCheck";
  } else {
    outputStream << "Test";
  }

  std::ostringstream indexStr;
  indexStr << " #" << Index << ":";
  outputStream << std::setw(3 + getNumWidth(TestHandler->GetMaxIndex()))
               << indexStr.str();
  outputStream << " ";

  return outputStream.str();
}

bool cmCTestRunTest::StartTest(std::unique_ptr<cmCTestRunTest> runner,
                               size_t completed, size_t total)
{
  auto* testRun = runner.get();

  testRun->TestProcess = cm::make_unique<cmProcess>(std::move(runner));

  if (!testRun->StartTest(completed, total)) {
    testRun->FinalizeTest(false);
    return false;
  }

  return true;
}

// Starts the execution of a test.  Returns once it has started
bool cmCTestRunTest::StartTest(size_t completed, size_t total)
{
  TotalNumberOfTests = total; // save for rerun case
  if (!CTest->GetTestProgressOutput()) {
    cmCTestLog(
      CTest, HANDLER_OUTPUT,
      std::setw(2 * getNumWidth(total) + 8)
        << "Start " << std::setw(getNumWidth(TestHandler->GetMaxIndex()))
        << TestProperties->Index << ": " << TestProperties->Name << std::endl);
  } else {
    std::string testName =
      GetTestPrefix(completed, total) + TestProperties->Name + "\n";
    cmCTestLog(CTest, HANDLER_TEST_PROGRESS_OUTPUT, testName);
  }

  ProcessOutput.clear();

  TestResult.Properties = TestProperties;
  TestResult.ExecutionTime = cmDuration::zero();
  TestResult.CompressOutput = false;
  TestResult.ReturnValue = -1;
  TestResult.TestCount = TestProperties->Index;
  TestResult.Name = TestProperties->Name;
  TestResult.Path = TestProperties->Directory;

  // Return immediately if test is disabled
  if (TestProperties->Disabled) {
    TestResult.CompletionStatus = "Disabled";
    TestResult.Status = cmCTestTestHandler::NOT_RUN;
    TestResult.Output = "Disabled";
    TestResult.FullCommandLine.clear();
    TestResult.Environment.clear();
    return false;
  }

  TestResult.CompletionStatus = "Failed to start";
  TestResult.Status = cmCTestTestHandler::BAD_COMMAND;

  // Check for failed fixture dependencies before we even look at the command
  // arguments because if we are not going to run the test, the command and
  // its arguments are irrelevant. This matters for the case where a fixture
  // dependency might be creating the executable we want to run.
  if (!FailedDependencies.empty()) {
    std::string msg = "Failed test dependencies:";
    for (std::string const& failedDep : FailedDependencies) {
      msg += " " + failedDep;
    }
    *TestHandler->LogFile << msg << std::endl;
    cmCTestLog(CTest, HANDLER_OUTPUT, msg << std::endl);
    TestResult.Output = msg;
    TestResult.FullCommandLine.clear();
    TestResult.Environment.clear();
    TestResult.CompletionStatus = "Fixture dependency failed";
    TestResult.Status = cmCTestTestHandler::NOT_RUN;
    return false;
  }

  ComputeArguments();
  std::vector<std::string>& args = TestProperties->Args;
  if (args.size() >= 2 && args[1] == "NOT_AVAILABLE") {
    std::string msg;
    if (CTest->GetConfigType().empty()) {
      msg = "Test not available without configuration.  (Missing \"-C "
            "<config>\"?)";
    } else {
      msg = cmStrCat("Test not available in configuration \"",
                     CTest->GetConfigType(), "\".");
    }
    *TestHandler->LogFile << msg << std::endl;
    cmCTestLog(CTest, ERROR_MESSAGE, msg << std::endl);
    TestResult.Output = msg;
    TestResult.FullCommandLine.clear();
    TestResult.Environment.clear();
    TestResult.CompletionStatus = "Missing Configuration";
    TestResult.Status = cmCTestTestHandler::NOT_RUN;
    return false;
  }

  // Check if all required files exist
  for (std::string const& file : TestProperties->RequiredFiles) {
    if (!cmSystemTools::FileExists(file)) {
      // Required file was not found
      *TestHandler->LogFile << "Unable to find required file: " << file
                            << std::endl;
      cmCTestLog(CTest, ERROR_MESSAGE,
                 "Unable to find required file: " << file << std::endl);
      TestResult.Output = "Unable to find required file: " + file;
      TestResult.FullCommandLine.clear();
      TestResult.Environment.clear();
      TestResult.CompletionStatus = "Required Files Missing";
      TestResult.Status = cmCTestTestHandler::NOT_RUN;
      return false;
    }
  }
  // log and return if we did not find the executable
  if (ActualCommand.empty()) {
    // if the command was not found create a TestResult object
    // that has that information
    *TestHandler->LogFile << "Unable to find executable: " << args[1]
                          << std::endl;
    cmCTestLog(CTest, ERROR_MESSAGE,
               "Unable to find executable: " << args[1] << std::endl);
    TestResult.Output = "Unable to find executable: " + args[1];
    TestResult.FullCommandLine.clear();
    TestResult.Environment.clear();
    TestResult.CompletionStatus = "Unable to find executable";
    TestResult.Status = cmCTestTestHandler::NOT_RUN;
    return false;
  }
  StartTime = CTest->CurrentTime();

  auto timeout = TestProperties->Timeout;

  TimeoutIsForStopTime = false;
  std::chrono::system_clock::time_point stop_time = CTest->GetStopTime();
  if (stop_time != std::chrono::system_clock::time_point()) {
    std::chrono::duration<double> stop_timeout =
      (stop_time - std::chrono::system_clock::now()) % std::chrono::hours(24);

    if (stop_timeout <= std::chrono::duration<double>::zero()) {
      stop_timeout = std::chrono::duration<double>::zero();
    }
    if (timeout == std::chrono::duration<double>::zero() ||
        stop_timeout < timeout) {
      TimeoutIsForStopTime = true;
      timeout = stop_timeout;
    }
  }

  return ForkProcess(timeout, TestProperties->ExplicitTimeout,
                     &TestProperties->Environment, &TestProperties->Affinity);
}

void cmCTestRunTest::ComputeArguments()
{
  Arguments.clear(); // reset because this might be a rerun
  auto j = TestProperties->Args.begin();
  ++j; // skip test name
  // find the test executable
  if (TestHandler->MemCheck) {
    cmCTestMemCheckHandler* handler =
      static_cast<cmCTestMemCheckHandler*>(TestHandler);
    ActualCommand = handler->MemoryTester;
    TestProperties->Args[1] =
      TestHandler->FindTheExecutable(TestProperties->Args[1]);
  } else {
    ActualCommand = TestHandler->FindTheExecutable(TestProperties->Args[1]);
    ++j; // skip the executable (it will be actualCommand)
  }
  std::string testCommand = cmSystemTools::ConvertToOutputPath(ActualCommand);

  // Prepends memcheck args to our command string
  TestHandler->GenerateTestCommand(Arguments, Index);
  for (std::string const& arg : Arguments) {
    testCommand += " \"";
    testCommand += arg;
    testCommand += "\"";
  }

  for (; j != TestProperties->Args.end(); ++j) {
    testCommand += " \"";
    testCommand += *j;
    testCommand += "\"";
    Arguments.push_back(*j);
  }
  TestResult.FullCommandLine = testCommand;

  // Print the test command in verbose mode
  cmCTestLog(CTest, HANDLER_VERBOSE_OUTPUT,
             std::endl
               << Index << ": "
               << (TestHandler->MemCheck ? "MemCheck" : "Test")
               << " command: " << testCommand << std::endl);

  // Print any test-specific env vars in verbose mode
  if (!TestProperties->Environment.empty()) {
    cmCTestLog(CTest, HANDLER_VERBOSE_OUTPUT,
               Index << ": "
                     << "Environment variables: " << std::endl);
  }
  for (std::string const& env : TestProperties->Environment) {
    cmCTestLog(CTest, HANDLER_VERBOSE_OUTPUT,
               Index << ":  " << env << std::endl);
  }
}

void cmCTestRunTest::DartProcessing()
{
  if (!ProcessOutput.empty() &&
      ProcessOutput.find("<DartMeasurement") != std::string::npos) {
    if (TestHandler->DartStuff.find(ProcessOutput)) {
      TestResult.DartString = TestHandler->DartStuff.match(1);
      // keep searching and replacing until none are left
      while (TestHandler->DartStuff1.find(ProcessOutput)) {
        // replace the exact match for the string
        cmSystemTools::ReplaceString(
          ProcessOutput, TestHandler->DartStuff1.match(1).c_str(), "");
      }
    }
  }
}

bool cmCTestRunTest::ForkProcess(cmDuration testTimeOut, bool explicitTimeout,
                                 std::vector<std::string>* environment,
                                 std::vector<size_t>* affinity)
{
  TestProcess->SetId(Index);
  TestProcess->SetWorkingDirectory(TestProperties->Directory);
  TestProcess->SetCommand(ActualCommand);
  TestProcess->SetCommandArguments(Arguments);

  // determine how much time we have
  cmDuration timeout = CTest->GetRemainingTimeAllowed();
  if (timeout != cmCTest::MaxDuration()) {
    timeout -= std::chrono::minutes(2);
  }
  if (CTest->GetTimeOut() > cmDuration::zero() &&
      CTest->GetTimeOut() < timeout) {
    timeout = CTest->GetTimeOut();
  }
  if (testTimeOut > cmDuration::zero() &&
      testTimeOut < CTest->GetRemainingTimeAllowed()) {
    timeout = testTimeOut;
  }
  // always have at least 1 second if we got to here
  if (timeout <= cmDuration::zero()) {
    timeout = std::chrono::seconds(1);
  }
  // handle timeout explicitly set to 0
  if (testTimeOut == cmDuration::zero() && explicitTimeout) {
    timeout = cmDuration::zero();
  }
  cmCTestOptionalLog(CTest, HANDLER_VERBOSE_OUTPUT,
                     Index << ": "
                           << "Test timeout computed to be: "
                           << cmDurationTo<unsigned int>(timeout) << "\n",
                     TestHandler->GetQuiet());

  TestProcess->SetTimeout(timeout);

#ifndef CMAKE_BOOTSTRAP
  cmSystemTools::SaveRestoreEnvironment sre;
#endif

  std::ostringstream envMeasurement;
  if (environment && !environment->empty()) {
    cmSystemTools::AppendEnv(*environment);
    for (auto const& var : *environment) {
      envMeasurement << var << std::endl;
    }
  }

  if (UseAllocatedResources) {
    std::vector<std::string> envLog;
    SetupResourcesEnvironment(&envLog);
    for (auto const& var : envLog) {
      envMeasurement << var << std::endl;
    }
  } else {
    cmSystemTools::UnsetEnv("CTEST_RESOURCE_GROUP_COUNT");
    // Signify that this variable is being actively unset
    envMeasurement << "#CTEST_RESOURCE_GROUP_COUNT=" << std::endl;
  }

  TestResult.Environment = envMeasurement.str();
  // Remove last newline
  TestResult.Environment.erase(TestResult.Environment.length() - 1);

  return TestProcess->StartProcess(MultiTestHandler.Loop, affinity);
}

void cmCTestRunTest::SetupResourcesEnvironment(std::vector<std::string>* log)
{
  std::string processCount = "CTEST_RESOURCE_GROUP_COUNT=";
  processCount += std::to_string(AllocatedResources.size());
  cmSystemTools::PutEnv(processCount);
  if (log) {
    log->push_back(processCount);
  }

  std::size_t i = 0;
  for (auto const& process : AllocatedResources) {
    std::string prefix = "CTEST_RESOURCE_GROUP_";
    prefix += std::to_string(i);
    std::string resourceList = prefix + '=';
    prefix += '_';
    bool firstType = true;
    for (auto const& it : process) {
      if (!firstType) {
        resourceList += ',';
      }
      firstType = false;
      auto resourceType = it.first;
      resourceList += resourceType;
      std::string var = prefix + cmSystemTools::UpperCase(resourceType) + '=';
      bool firstName = true;
      for (auto const& it2 : it.second) {
        if (!firstName) {
          var += ';';
        }
        firstName = false;
        var += "id:" + it2.Id + ",slots:" + std::to_string(it2.Slots);
      }
      cmSystemTools::PutEnv(var);
      if (log) {
        log->push_back(var);
      }
    }
    cmSystemTools::PutEnv(resourceList);
    if (log) {
      log->push_back(resourceList);
    }
    ++i;
  }
}

void cmCTestRunTest::WriteLogOutputTop(size_t completed, size_t total)
{
  std::ostringstream outputStream;

  // If this is the last or only run of this test, or progress output is
  // requested, then print out completed / total.
  // Only issue is if a test fails and we are running until fail
  // then it will never print out the completed / total, same would
  // got for run until pass.  Trick is when this is called we don't
  // yet know if we are passing or failing.
  bool const progressOnLast = (RepeatMode != cmCTest::Repeat::UntilPass &&
                               RepeatMode != cmCTest::Repeat::AfterTimeout);
  if ((progressOnLast && NumberOfRunsLeft == 1) ||
      (!progressOnLast && NumberOfRunsLeft == NumberOfRunsTotal) ||
      CTest->GetTestProgressOutput()) {
    outputStream << std::setw(getNumWidth(total)) << completed << "/";
    outputStream << std::setw(getNumWidth(total)) << total << " ";
  }
  // if this is one of several runs of a test just print blank space
  // to keep things neat
  else {
    outputStream << std::setw(getNumWidth(total)) << "  ";
    outputStream << std::setw(getNumWidth(total)) << "  ";
  }

  if (TestHandler->MemCheck) {
    outputStream << "MemCheck";
  } else {
    outputStream << "Test";
  }

  std::ostringstream indexStr;
  indexStr << " #" << Index << ":";
  outputStream << std::setw(3 + getNumWidth(TestHandler->GetMaxIndex()))
               << indexStr.str();
  outputStream << " ";

  const int maxTestNameWidth = CTest->GetMaxTestNameWidth();
  std::string outname = TestProperties->Name + " ";
  outname.resize(maxTestNameWidth + 4, '.');
  outputStream << outname;

  *TestHandler->LogFile << TestProperties->Index << "/"
                        << TestHandler->TotalNumberOfTests
                        << " Testing: " << TestProperties->Name << std::endl;
  *TestHandler->LogFile << TestProperties->Index << "/"
                        << TestHandler->TotalNumberOfTests
                        << " Test: " << TestProperties->Name << std::endl;
  *TestHandler->LogFile << "Command: \"" << ActualCommand << "\"";

  for (std::string const& arg : Arguments) {
    *TestHandler->LogFile << " \"" << arg << "\"";
  }
  *TestHandler->LogFile << std::endl
                        << "Directory: " << TestProperties->Directory
                        << std::endl
                        << "\"" << TestProperties->Name
                        << "\" start time: " << StartTime << std::endl;

  *TestHandler->LogFile
    << "Output:" << std::endl
    << "----------------------------------------------------------"
    << std::endl;
  *TestHandler->LogFile << ProcessOutput << "<end of output>" << std::endl;

  if (!CTest->GetTestProgressOutput()) {
    cmCTestLog(CTest, HANDLER_OUTPUT, outputStream.str());
  }

  cmCTestLog(CTest, DEBUG, "Testing " << TestProperties->Name << " ... ");
}

void cmCTestRunTest::FinalizeTest(bool started)
{
  MultiTestHandler.FinishTestProcess(TestProcess->GetRunner(), started);
}
