Simplify starting subCreator process

Introduce SubCreatorConfig class that helps setting up
a Creator subprocess. This limits the code repetition.

Introduce SUB_CREATOR_PROCESS(subProcess) macro to easily define
environment variables referred to Creator subprocesses.
This macro also forward declares the subProcessMain()
function.

Match names of main subprocess functions with their
corresponding environment variables.

Group subProcessMain() functions near the corresponding
test function for clarity.

Change-Id: Ib4365cf18fddc1527ebc99accee1fbb974bbf7a1
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
Jarek Kobus
2022-03-04 17:14:13 +01:00
parent 809b371108
commit bbb0270fb1

View File

@@ -29,6 +29,7 @@
#include <utils/hostosinfo.h> #include <utils/hostosinfo.h>
#include <utils/launcherinterface.h> #include <utils/launcherinterface.h>
#include <utils/porting.h> #include <utils/porting.h>
#include <utils/qtcassert.h>
#include <utils/qtcprocess.h> #include <utils/qtcprocess.h>
#include <utils/singleton.h> #include <utils/singleton.h>
#include <utils/stringutils.h> #include <utils/stringutils.h>
@@ -46,21 +47,24 @@
#include <fcntl.h> #include <fcntl.h>
#endif #endif
using namespace Utils; using namespace Utils;
#define SUB_CREATOR_PROCESS(var)\
const char var[] = "TST_QTC_PROCESS_" QTC_ASSERT_STRINGIFY(var);\
static void var ## Main();
// Many tests in this file need to start a new subprocess with custom code. // Many tests in this file need to start a new subprocess with custom code.
// In order to simplify things we don't produce separate executables, but invoke // In order to simplify things we don't produce separate executables, but invoke
// the same test process recursively and prior to the execution we set one of the // the same test process recursively and prior to the execution we set one of the
// following environment variables: // following environment variables:
const char kExitCodeSubProcessCode[] = "QTC_TST_QTCPROCESS_EXITCODE_CODE"; SUB_CREATOR_PROCESS(subExitCode);
const char kRunBlockingStdOutSubProcessWithEndl[] = "QTC_TST_QTCPROCESS_RUNBLOCKINGSTDOUT_WITHENDL"; SUB_CREATOR_PROCESS(subRunBlockingStdOut);
const char kLineCallback[] = "QTC_TST_QTCPROCESS_LINECALLBACK"; SUB_CREATOR_PROCESS(subLineCallback);
const char kTestProcess[] = "QTC_TST_TEST_PROCESS"; SUB_CREATOR_PROCESS(subSimpleTest);
const char kForwardProcess[] = "QTC_TST_FORWARD_PROCESS"; SUB_CREATOR_PROCESS(subProcessChannelForwarding);
const char kForwardSubProcess[] = "QTC_TST_FORWARD_SUB_PROCESS"; SUB_CREATOR_PROCESS(subSubProcessChannelForwarding);
const char kBlockingProcess[] = "QTC_TST_BLOCKING_PROCESS"; SUB_CREATOR_PROCESS(subKillBlockingProcess);
// The variables above are meant to be different custom executables. Inside initTestCase() // The variables above are meant to be different custom executables. Inside initTestCase()
// we are detecting if one of these variables is set and invoke directly a respective custom // we are detecting if one of these variables is set and invoke directly a respective custom
@@ -68,7 +72,7 @@ const char kBlockingProcess[] = "QTC_TST_BLOCKING_PROCESS";
// the recursion, as from the test point of view we meant to execute only our custom code // the recursion, as from the test point of view we meant to execute only our custom code
// without further execution of the test itself. // without further execution of the test itself.
const char testProcessData[] = "Test process successfully executed."; const char simpleTestData[] = "Test process successfully executed.";
const char forwardedOutputData[] = "This is the output message."; const char forwardedOutputData[] = "This is the output message.";
const char forwardedErrorData[] = "This is the error message."; const char forwardedErrorData[] = "This is the error message.";
const char runBlockingStdOutSubProcessMagicWord[] = "42"; const char runBlockingStdOutSubProcessMagicWord[] = "42";
@@ -81,82 +85,62 @@ const char lineCallbackData[] =
"Rebasing (1/10)\r| <delay> Rebasing (2/10)\r| <delay> ...\r\n|" "Rebasing (1/10)\r| <delay> Rebasing (2/10)\r| <delay> ...\r\n|"
"And no end"; "And no end";
static void exitCodeSubProcessMain() static Environment subEnvironment(const char *envVar, const QString &envVal)
{
const int exitCode = qEnvironmentVariableIntValue(kExitCodeSubProcessCode);
std::cout << "Exiting with code:" << exitCode << std::endl;
exit(exitCode);
}
static void blockingStdOutSubProcessMain()
{
std::cout << "Wait for the Answer to the Ultimate Question of Life, "
"The Universe, and Everything..." << std::endl;
QThread::msleep(300);
std::cout << runBlockingStdOutSubProcessMagicWord << "...Now wait for the question...";
if (qEnvironmentVariable(kRunBlockingStdOutSubProcessWithEndl) == "true")
std::cout << std::endl;
QThread::msleep(5000);
exit(0);
}
static void lineCallbackMain()
{
#ifdef Q_OS_WIN
// Prevent \r\n -> \r\r\n translation.
setmode(fileno(stderr), O_BINARY);
#endif
fprintf(stderr, "%s", QByteArray(lineCallbackData).replace('|', "").data());
exit(0);
}
static void testProcessSubProcessMain()
{
std::cout << testProcessData << std::endl;
exit(0);
}
// Since we want to test whether the process forwards its channels or not, we can't just create
// a process and start it, because in this case there is no way on how to check whether something
// went into out output channels or not.
// So we start two processes in chain instead. On the beginning the processChannelForwarding()
// test starts the "testForwardProcessMain" - this one will start another process
// "testForwardSubProcessMain" with forwarding options. The "testForwardSubProcessMain"
// is very simple - it just puts something to the output and the error channels.
// Then "testForwardProcessMain" either forwards these channels or not - we check it in the outer
// processChannelForwarding() test.
static void testForwardProcessMain()
{ {
Environment env = Environment::systemEnvironment(); Environment env = Environment::systemEnvironment();
env.set(kForwardSubProcess, {}); env.set(envVar, envVal);
return env;
}
static CommandLine subCommandLine()
{
QStringList args = QCoreApplication::arguments(); QStringList args = QCoreApplication::arguments();
const QString binary = args.takeFirst(); const QString binary = args.takeFirst();
const CommandLine command(FilePath::fromString(binary), args); return CommandLine(FilePath::fromString(binary), args);
QtcProcess process;
const QProcess::ProcessChannelMode channelMode
= QProcess::ProcessChannelMode(qEnvironmentVariableIntValue(kForwardProcess));
process.setProcessChannelMode(channelMode);
process.setCommand(command);
process.setEnvironment(env);
process.start();
process.waitForFinished();
exit(0);
} }
static void testForwardSubProcessMain() class SubCreatorConfig
{ {
std::cout << forwardedOutputData << std::endl; public:
std::cerr << forwardedErrorData << std::endl; SubCreatorConfig(const char *envVar, const QString &envVal)
exit(0); : m_environment(subEnvironment(envVar, envVal))
} , m_commandLine(subCommandLine()) {}
static void blockingProcessSubProcessMain() void setupSubProcess(QtcProcess *subProcess)
{ {
std::cout << "Blocking process successfully executed." << std::endl; subProcess->setEnvironment(m_environment);
while (true) subProcess->setCommand(m_commandLine);
; }
private:
const Environment m_environment;
const CommandLine m_commandLine;
};
static std::atomic_int s_processCounter = 0;
static const bool s_removeSingletonsOnLastProcess = false;
// The use of this class (when s_removeSingletonsOnLastProcess = true) guarantees that if all
// instances of QtcProcess are destructed at some point (i.e. after each test function ends)
// we also run process reaper's and process launcher's destructors. Otherwise
// (when s_removeSingletonsOnLastProcess = true) this is a no-op wrapper around QtcProcess.
class TestProcess : public QtcProcess
{
public:
class TestProcessDeleter : public QObject
{
public:
TestProcessDeleter(QObject *parent) : QObject(parent) { s_processCounter.fetch_add(1); }
~TestProcessDeleter() {
if ((s_processCounter.fetch_sub(1) == 1) && s_removeSingletonsOnLastProcess)
Utils::Singleton::deleteAll();
}
};
TestProcess() { new TestProcessDeleter(this); }
};
static void subSimpleTestMain()
{
std::cout << simpleTestData << std::endl;
exit(0);
} }
class MacroMapExpander : public AbstractMacroExpander { class MacroMapExpander : public AbstractMacroExpander {
@@ -233,20 +217,20 @@ void tst_QtcProcess::initTestCase()
+ Core::Constants::IDE_CASED_ID + "-XXXXXX"); + Core::Constants::IDE_CASED_ID + "-XXXXXX");
Utils::LauncherInterface::setPathToLauncher(qApp->applicationDirPath() + '/' Utils::LauncherInterface::setPathToLauncher(qApp->applicationDirPath() + '/'
+ QLatin1String(TEST_RELATIVE_LIBEXEC_PATH)); + QLatin1String(TEST_RELATIVE_LIBEXEC_PATH));
if (qEnvironmentVariableIsSet(kExitCodeSubProcessCode)) if (qEnvironmentVariableIsSet(subExitCode))
exitCodeSubProcessMain(); subExitCodeMain();
if (qEnvironmentVariableIsSet(kRunBlockingStdOutSubProcessWithEndl)) if (qEnvironmentVariableIsSet(subRunBlockingStdOut))
blockingStdOutSubProcessMain(); subRunBlockingStdOutMain();
if (qEnvironmentVariableIsSet(kLineCallback)) if (qEnvironmentVariableIsSet(subLineCallback))
lineCallbackMain(); subLineCallbackMain();
if (qEnvironmentVariableIsSet(kTestProcess)) if (qEnvironmentVariableIsSet(subSimpleTest))
testProcessSubProcessMain(); subSimpleTestMain();
if (qEnvironmentVariableIsSet(kForwardSubProcess)) if (qEnvironmentVariableIsSet(subProcessChannelForwarding))
testForwardSubProcessMain(); subProcessChannelForwardingMain();
else if (qEnvironmentVariableIsSet(kForwardProcess)) if (qEnvironmentVariableIsSet(subSubProcessChannelForwarding))
testForwardProcessMain(); subSubProcessChannelForwardingMain();
if (qEnvironmentVariableIsSet(kBlockingProcess)) if (qEnvironmentVariableIsSet(subKillBlockingProcess))
blockingProcessSubProcessMain(); subKillBlockingProcessMain();
homeStr = QLatin1String("@HOME@"); homeStr = QLatin1String("@HOME@");
home = QDir::homePath(); home = QDir::homePath();
@@ -907,6 +891,13 @@ void tst_QtcProcess::iteratorEditsLinux()
iteratorEditsHelper(OsTypeLinux); iteratorEditsHelper(OsTypeLinux);
} }
static void subExitCodeMain()
{
const int exitCode = qEnvironmentVariableIntValue(subExitCode);
std::cout << "Exiting with code:" << exitCode << std::endl;
exit(exitCode);
}
void tst_QtcProcess::exitCode_data() void tst_QtcProcess::exitCode_data()
{ {
QTest::addColumn<int>("exitCode"); QTest::addColumn<int>("exitCode");
@@ -925,34 +916,39 @@ void tst_QtcProcess::exitCode()
{ {
QFETCH(int, exitCode); QFETCH(int, exitCode);
Environment env = Environment::systemEnvironment(); SubCreatorConfig subConfig(subExitCode, QString::number(exitCode));
env.set(kExitCodeSubProcessCode, QString::number(exitCode));
QStringList args = QCoreApplication::arguments();
const QString binary = args.takeFirst();
const CommandLine command(FilePath::fromString(binary), args);
{ {
QtcProcess qtcP; TestProcess process;
qtcP.setCommand(command); subConfig.setupSubProcess(&process);
qtcP.setEnvironment(env); process.start();
qtcP.start(); const bool finished = process.waitForFinished();
const bool finished = qtcP.waitForFinished();
QVERIFY(finished); QVERIFY(finished);
QCOMPARE(qtcP.exitCode(), exitCode); QCOMPARE(process.exitCode(), exitCode);
QCOMPARE(qtcP.exitCode() == 0, qtcP.result() == ProcessResult::FinishedWithSuccess); QCOMPARE(process.exitCode() == 0, process.result() == ProcessResult::FinishedWithSuccess);
} }
{ {
QtcProcess sP; TestProcess process;
sP.setCommand(command); subConfig.setupSubProcess(&process);
sP.setEnvironment(env); process.runBlocking();
sP.runBlocking();
QCOMPARE(sP.exitCode(), exitCode); QCOMPARE(process.exitCode(), exitCode);
QCOMPARE(sP.exitCode() == 0, sP.result() == ProcessResult::FinishedWithSuccess); QCOMPARE(process.exitCode() == 0, process.result() == ProcessResult::FinishedWithSuccess);
} }
} }
static void subRunBlockingStdOutMain()
{
std::cout << "Wait for the Answer to the Ultimate Question of Life, "
"The Universe, and Everything..." << std::endl;
QThread::msleep(300);
std::cout << runBlockingStdOutSubProcessMagicWord << "...Now wait for the question...";
if (qEnvironmentVariable(subRunBlockingStdOut) == "true")
std::cout << std::endl;
QThread::msleep(5000);
exit(0);
}
void tst_QtcProcess::runBlockingStdOut_data() void tst_QtcProcess::runBlockingStdOut_data()
{ {
QTest::addColumn<bool>("withEndl"); QTest::addColumn<bool>("withEndl");
@@ -974,40 +970,44 @@ void tst_QtcProcess::runBlockingStdOut()
QFETCH(bool, withEndl); QFETCH(bool, withEndl);
QFETCH(int, timeOutS); QFETCH(int, timeOutS);
QtcProcess sp; SubCreatorConfig subConfig(subRunBlockingStdOut, withEndl ? "true" : "false");
QStringList args = QCoreApplication::arguments(); TestProcess process;
const QString binary = args.takeFirst(); subConfig.setupSubProcess(&process);
sp.setCommand(CommandLine(FilePath::fromString(binary), args));
Environment env = Environment::systemEnvironment(); process.setTimeoutS(timeOutS);
env.set(kRunBlockingStdOutSubProcessWithEndl, withEndl ? "true" : "false");
sp.setEnvironment(env);
sp.setTimeoutS(timeOutS);
bool readLastLine = false; bool readLastLine = false;
sp.setStdOutCallback([&readLastLine, &sp](const QString &out) { process.setStdOutCallback([&readLastLine, &process](const QString &out) {
if (out.startsWith(runBlockingStdOutSubProcessMagicWord)) { if (out.startsWith(runBlockingStdOutSubProcessMagicWord)) {
readLastLine = true; readLastLine = true;
sp.kill(); process.kill();
} }
}); });
sp.runBlocking(); process.runBlocking();
// See also QTCREATORBUG-25667 for why it is a bad idea to use QtcProcess::runBlocking // See also QTCREATORBUG-25667 for why it is a bad idea to use QtcProcess::runBlocking
// with interactive cli tools. // with interactive cli tools.
QEXPECT_FAIL("Unterminated stdout lost: early timeout", "", Continue); QEXPECT_FAIL("Unterminated stdout lost: early timeout", "", Continue);
QVERIFY2(sp.result() != ProcessResult::Hang, "Process run did not time out."); QVERIFY2(process.result() != ProcessResult::Hang, "Process run did not time out.");
QEXPECT_FAIL("Unterminated stdout lost: early timeout", "", Continue); QEXPECT_FAIL("Unterminated stdout lost: early timeout", "", Continue);
QVERIFY2(readLastLine, "Last line was read."); QVERIFY2(readLastLine, "Last line was read.");
} }
static void subLineCallbackMain()
{
#ifdef Q_OS_WIN
// Prevent \r\n -> \r\r\n translation.
setmode(fileno(stderr), O_BINARY);
#endif
fprintf(stderr, "%s", QByteArray(lineCallbackData).replace('|', "").data());
exit(0);
}
void tst_QtcProcess::lineCallback() void tst_QtcProcess::lineCallback()
{ {
QtcProcess process; SubCreatorConfig subConfig(subLineCallback, {});
QStringList args = QCoreApplication::arguments(); TestProcess process;
const QString binary = args.takeFirst(); subConfig.setupSubProcess(&process);
process.setCommand(CommandLine(FilePath::fromString(binary), args));
Environment env = Environment::systemEnvironment();
env.set(kLineCallback, "Yes");
process.setEnvironment(env);
QStringList lines = QString(lineCallbackData).split('|'); QStringList lines = QString(lineCallbackData).split('|');
int lineNumber = 0; int lineNumber = 0;
process.setStdErrLineCallback([lines, &lineNumber](const QString &actual) { process.setStdErrLineCallback([lines, &lineNumber](const QString &actual) {
@@ -1027,7 +1027,7 @@ void tst_QtcProcess::lineCallback()
void tst_QtcProcess::lineCallbackIntern() void tst_QtcProcess::lineCallbackIntern()
{ {
QtcProcess process; TestProcess process;
QStringList lines = QString(lineCallbackData).split('|'); QStringList lines = QString(lineCallbackData).split('|');
int lineNumber = 0; int lineNumber = 0;
process.setStdOutLineCallback([lines, &lineNumber](const QString &actual) { process.setStdOutLineCallback([lines, &lineNumber](const QString &actual) {
@@ -1048,15 +1048,10 @@ void tst_QtcProcess::lineCallbackIntern()
void tst_QtcProcess::waitForStartedAndFinished() void tst_QtcProcess::waitForStartedAndFinished()
{ {
Environment env = Environment::systemEnvironment(); SubCreatorConfig subConfig(subSimpleTest, {});
env.set(kTestProcess, {}); TestProcess process;
QStringList args = QCoreApplication::arguments(); subConfig.setupSubProcess(&process);
const QString binary = args.takeFirst();
const CommandLine command(FilePath::fromString(binary), args);
QtcProcess process;
process.setCommand(command);
process.setEnvironment(env);
process.start(); process.start();
QThread::msleep(1000); // long enough for process to finish QThread::msleep(1000); // long enough for process to finish
QVERIFY(process.waitForStarted()); QVERIFY(process.waitForStarted());
@@ -1067,7 +1062,7 @@ void tst_QtcProcess::waitForStartedAndFinished()
void tst_QtcProcess::notRunningAfterStartingNonExistingProgram() void tst_QtcProcess::notRunningAfterStartingNonExistingProgram()
{ {
QtcProcess process; TestProcess process;
process.setCommand({ FilePath::fromString( process.setCommand({ FilePath::fromString(
"there_is_a_big_chance_that_executable_with_that_name_does_not_exists"), {} }); "there_is_a_big_chance_that_executable_with_that_name_does_not_exists"), {} });
@@ -1096,6 +1091,40 @@ void tst_QtcProcess::notRunningAfterStartingNonExistingProgram()
} }
} }
// Since we want to test whether the process forwards its channels or not, we can't just create
// a process and start it, because in this case there is no way on how to check whether something
// went into our output channels or not.
// So we start two processes in chain instead. On the beginning the processChannelForwarding()
// test starts the "subProcessChannelForwardingMain" - this one will start another process
// "subSubProcessChannelForwardingMain" with forwarding options.
// The "subSubProcessChannelForwardingMain" is very simple - it just puts something to the output
// and the error channels. Then "subProcessChannelForwardingMain" either forwards these channels
// or not - we check it in the outer processChannelForwarding() test.
static void subSubProcessChannelForwardingMain()
{
std::cout << forwardedOutputData << std::endl;
std::cerr << forwardedErrorData << std::endl;
exit(0);
}
static void subProcessChannelForwardingMain()
{
const QProcess::ProcessChannelMode channelMode
= QProcess::ProcessChannelMode(qEnvironmentVariableIntValue(subProcessChannelForwarding));
qunsetenv(subProcessChannelForwarding);
SubCreatorConfig subConfig(subSubProcessChannelForwarding, {});
TestProcess process;
subConfig.setupSubProcess(&process);
process.setProcessChannelMode(channelMode);
process.start();
process.waitForFinished();
exit(0);
}
void tst_QtcProcess::processChannelForwarding_data() void tst_QtcProcess::processChannelForwarding_data()
{ {
QTest::addColumn<QProcess::ProcessChannelMode>("channelMode"); QTest::addColumn<QProcess::ProcessChannelMode>("channelMode");
@@ -1114,15 +1143,10 @@ void tst_QtcProcess::processChannelForwarding()
QFETCH(bool, outputForwarded); QFETCH(bool, outputForwarded);
QFETCH(bool, errorForwarded); QFETCH(bool, errorForwarded);
Environment env = Environment::systemEnvironment(); SubCreatorConfig subConfig(subProcessChannelForwarding, QString::number(int(channelMode)));
env.set(kForwardProcess, QString::number(int(channelMode))); TestProcess process;
QStringList args = QCoreApplication::arguments(); subConfig.setupSubProcess(&process);
const QString binary = args.takeFirst();
const CommandLine command(FilePath::fromString(binary), args);
QtcProcess process;
process.setCommand(command);
process.setEnvironment(env);
process.start(); process.start();
QVERIFY(process.waitForFinished()); QVERIFY(process.waitForFinished());
@@ -1171,19 +1195,21 @@ protected:
bool MessageHandler::ok = true; bool MessageHandler::ok = true;
QtMessageHandler MessageHandler::oldMessageHandler = 0; QtMessageHandler MessageHandler::oldMessageHandler = 0;
static void subKillBlockingProcessMain()
{
std::cout << "Blocking process successfully executed." << std::endl;
while (true)
;
}
void tst_QtcProcess::killBlockingProcess() void tst_QtcProcess::killBlockingProcess()
{ {
Environment env = Environment::systemEnvironment(); SubCreatorConfig subConfig(subKillBlockingProcess, {});
env.set(kBlockingProcess, {});
QStringList args = QCoreApplication::arguments();
const QString binary = args.takeFirst();
const CommandLine command(FilePath::fromString(binary), args);
MessageHandler msgHandler; MessageHandler msgHandler;
{ {
QtcProcess process; TestProcess process;
process.setCommand(command); subConfig.setupSubProcess(&process);
process.setEnvironment(env);
process.start(); process.start();
QVERIFY(process.waitForStarted()); QVERIFY(process.waitForStarted());
QVERIFY(!process.waitForFinished(1000)); QVERIFY(!process.waitForFinished(1000));
@@ -1194,15 +1220,10 @@ void tst_QtcProcess::killBlockingProcess()
void tst_QtcProcess::flushFinishedWhileWaitingForReadyRead() void tst_QtcProcess::flushFinishedWhileWaitingForReadyRead()
{ {
Environment env = Environment::systemEnvironment(); SubCreatorConfig subConfig(subSimpleTest, {});
env.set(kTestProcess, {}); TestProcess process;
QStringList args = QCoreApplication::arguments(); subConfig.setupSubProcess(&process);
const QString binary = args.takeFirst();
const CommandLine command(FilePath::fromString(binary), args);
QtcProcess process;
process.setCommand(command);
process.setEnvironment(env);
process.start(); process.start();
QVERIFY(process.waitForStarted()); QVERIFY(process.waitForStarted());
@@ -1219,7 +1240,7 @@ void tst_QtcProcess::flushFinishedWhileWaitingForReadyRead()
QCOMPARE(process.state(), QProcess::NotRunning); QCOMPARE(process.state(), QProcess::NotRunning);
QVERIFY(!timer.hasExpired()); QVERIFY(!timer.hasExpired());
QVERIFY(reply.contains(testProcessData)); QVERIFY(reply.contains(simpleTestData));
} }
QTEST_MAIN(tst_QtcProcess) QTEST_MAIN(tst_QtcProcess)