diff options
Diffstat (limited to 'tests/auto/corelib/io/qprocess/tst_qprocess.cpp')
-rw-r--r-- | tests/auto/corelib/io/qprocess/tst_qprocess.cpp | 735 |
1 files changed, 654 insertions, 81 deletions
diff --git a/tests/auto/corelib/io/qprocess/tst_qprocess.cpp b/tests/auto/corelib/io/qprocess/tst_qprocess.cpp index 4506e18f8b..316c388bc8 100644 --- a/tests/auto/corelib/io/qprocess/tst_qprocess.cpp +++ b/tests/auto/corelib/io/qprocess/tst_qprocess.cpp @@ -1,6 +1,6 @@ // Copyright (C) 2021 The Qt Company Ltd. // Copyright (C) 2022 Intel Corporation. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QTest> #include <QTestEventLoop> @@ -21,12 +21,17 @@ #include <qplatformdefs.h> #ifdef Q_OS_UNIX # include <private/qcore_unix_p.h> +# include <sys/wait.h> #endif #include <QtTest/private/qemulationdetector_p.h> #include <stdlib.h> +#include "crasher.h" + +using namespace Qt::StringLiterals; + typedef void (QProcess::*QProcessErrorSignal)(QProcess::ProcessError); class tst_QProcess : public QObject @@ -42,8 +47,8 @@ private slots: void getSetCheck(); void constructing(); void simpleStart(); - void setChildProcessModifier(); void startCommand(); + void startCommandEmptyString(); void startWithOpen(); void startWithOldOpen(); void execute(); @@ -84,8 +89,11 @@ private slots: void environmentIsSorted(); void spaceInName(); void setStandardInputFile(); + void setStandardInputFileFailure(); void setStandardOutputFile_data(); void setStandardOutputFile(); + void setStandardOutputFileFailure_data() { setStandardOutputFile_data(); } + void setStandardOutputFileFailure(); void setStandardOutputFileNullDevice(); void setStandardOutputFileAndWaitForBytesWritten(); void setStandardOutputProcess_data(); @@ -111,6 +119,22 @@ private slots: void nativeArguments(); void createProcessArgumentsModifier(); #endif // Q_OS_WIN +#if defined(Q_OS_UNIX) + void setChildProcessModifier_data(); + void setChildProcessModifier(); + void failChildProcessModifier_data() { setChildProcessModifier_data(); } + void failChildProcessModifier(); + void throwInChildProcessModifier(); + void terminateInChildProcessModifier_data(); + void terminateInChildProcessModifier(); + void raiseInChildProcessModifier(); + void unixProcessParameters_data(); + void unixProcessParameters(); + void impossibleUnixProcessParameters_data(); + void impossibleUnixProcessParameters(); + void unixProcessParametersAndChildModifier(); + void unixProcessParametersOtherFileDescriptors(); +#endif void exitCodeTest(); void systemEnvironment(); void lockupsInStartDetached(); @@ -148,16 +172,28 @@ protected slots: void waitForBytesWrittenInABytesWrittenSlotSlot(); private: + QString nonExistentFileName = u"/this/file/cant/exist/hopefully"_s; + qint64 bytesAvailable; QTemporaryDir m_temporaryDir; + bool haveWorkingVFork = false; }; void tst_QProcess::initTestCase() { +#if defined(QT_ASAN_ENABLED) + QSKIP("Skipping QProcess tests under ASAN as they are flaky (QTBUG-109329)"); +#endif QVERIFY2(m_temporaryDir.isValid(), qPrintable(m_temporaryDir.errorString())); // chdir to our testdata path and execute helper apps relative to that. QString testdata_dir = QFileInfo(QFINDTESTDATA("testProcessNormal")).absolutePath(); QVERIFY2(QDir::setCurrent(testdata_dir), qPrintable("Could not chdir to " + testdata_dir)); + +#if defined(Q_OS_LINUX) && QT_CONFIG(forkfd_pidfd) + // see detect_clone_pidfd_support() in forkfd_linux.c for explanation + waitid(/*P_PIDFD*/ idtype_t(3), INT_MAX, NULL, WEXITED|WNOHANG); + haveWorkingVFork = (errno == EBADF); +#endif } void tst_QProcess::cleanupTestCase() @@ -250,44 +286,6 @@ void tst_QProcess::simpleStart() QCOMPARE(qvariant_cast<QProcess::ProcessState>(spy.at(2).at(0)), QProcess::NotRunning); } -#ifdef Q_OS_UNIX -static const char messageFromChildProcess[] = "Message from the child process"; -static void childProcessModifier(int fd) -{ - QT_WRITE(fd, messageFromChildProcess, sizeof(messageFromChildProcess) - 1); - QT_CLOSE(fd); -} -#endif - -void tst_QProcess::setChildProcessModifier() -{ -#ifdef Q_OS_UNIX - int pipes[2] = { -1 , -1 }; - QVERIFY(qt_safe_pipe(pipes) == 0); - - QProcess process; - process.setChildProcessModifier([pipes]() { - ::childProcessModifier(pipes[1]); - }); - process.start("testProcessNormal/testProcessNormal"); - if (process.state() != QProcess::Starting) - QCOMPARE(process.state(), QProcess::Running); - QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString())); - - char buf[sizeof messageFromChildProcess] = {}; - qt_safe_close(pipes[1]); - QCOMPARE(qt_safe_read(pipes[0], buf, sizeof(buf)), qint64(sizeof(messageFromChildProcess)) - 1); - QCOMPARE(buf, messageFromChildProcess); - qt_safe_close(pipes[0]); - - QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString())); - QCOMPARE(process.exitStatus(), QProcess::NormalExit); - QCOMPARE(process.exitCode(), 0); -#else - QSKIP("Unix-only test"); -#endif -} - void tst_QProcess::startCommand() { QProcess process; @@ -302,6 +300,25 @@ void tst_QProcess::startCommand() QCOMPARE(actual, expected); } +void tst_QProcess::startCommandEmptyString() +{ + static const char warningMsg[] = + "QProcess::startCommand: empty or whitespace-only command was provided"; + QProcess process; + + QTest::ignoreMessage(QtWarningMsg, warningMsg); + process.startCommand(""); + QVERIFY(!process.waitForStarted()); + + QTest::ignoreMessage(QtWarningMsg, warningMsg); + process.startCommand(" "); + QVERIFY(!process.waitForStarted()); + + QTest::ignoreMessage(QtWarningMsg, warningMsg); + process.startCommand("\t\n"); + QVERIFY(!process.waitForStarted()); +} + void tst_QProcess::startWithOpen() { QProcess p; @@ -1469,6 +1486,479 @@ void tst_QProcess::createProcessArgumentsModifier() } #endif // Q_OS_WIN +#ifdef Q_OS_UNIX +static constexpr int sigs[] = { SIGABRT, SIGILL, SIGSEGV }; +struct DisableCrashLogger +{ + // disable core dumps too + tst_QProcessCrash::NoCoreDumps disableCoreDumps {}; + std::array<struct sigaction, std::size(sigs)> oldhandlers; + DisableCrashLogger() + { + struct sigaction def = {}; + def.sa_handler = SIG_DFL; + for (uint i = 0; i < std::size(sigs); ++i) + sigaction(sigs[i], &def, &oldhandlers[i]); + } + ~DisableCrashLogger() + { + // restore them + for (uint i = 0; i < std::size(sigs); ++i) + sigaction(sigs[i], &oldhandlers[i], nullptr); + } +}; + +QT_BEGIN_NAMESPACE +Q_AUTOTEST_EXPORT bool _qprocessUsingVfork() noexcept; +QT_END_NAMESPACE +static constexpr char messageFromChildProcess[] = "Message from the child process"; +static_assert(std::char_traits<char>::length(messageFromChildProcess) <= PIPE_BUF); +static void childProcessModifier(int fd) +{ + QT_WRITE(fd, messageFromChildProcess, strlen(messageFromChildProcess)); + QT_CLOSE(fd); +} + +void tst_QProcess::setChildProcessModifier_data() +{ + QTest::addColumn<bool>("detached"); + QTest::addColumn<bool>("useVfork"); + QTest::newRow("normal") << false << false; + QTest::newRow("detached") << true << false; + +#ifdef QT_BUILD_INTERNAL + if (_qprocessUsingVfork()) { + QTest::newRow("normal-vfork") << false << true; + QTest::newRow("detached-vfork") << true << true; + } +#endif +} + +void tst_QProcess::setChildProcessModifier() +{ + QFETCH(bool, detached); + QFETCH(bool, useVfork); + int pipes[2] = { -1 , -1 }; + QVERIFY(qt_safe_pipe(pipes) == 0); + + QProcess process; + if (useVfork) + process.setUnixProcessParameters(QProcess::UnixProcessFlag::UseVFork); + process.setChildProcessModifier([pipes]() { + ::childProcessModifier(pipes[1]); + }); + process.setProgram("testProcessNormal/testProcessNormal"); + if (detached) { + process.startDetached(); + } else { + process.start("testProcessNormal/testProcessNormal"); + if (process.state() != QProcess::Starting) + QCOMPARE(process.state(), QProcess::Running); + QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString())); + QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString())); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); + QCOMPARE(process.exitCode(), 0); + } + + char buf[sizeof messageFromChildProcess] = {}; + qt_safe_close(pipes[1]); + QCOMPARE(qt_safe_read(pipes[0], buf, sizeof(buf)), qint64(sizeof(messageFromChildProcess)) - 1); + QCOMPARE(buf, messageFromChildProcess); + qt_safe_close(pipes[0]); +} + +void tst_QProcess::failChildProcessModifier() +{ + static const char failureMsg[] = + "Some error message from the child process would go here if this were a " + "real application"; + static_assert(sizeof(failureMsg) < _POSIX_PIPE_BUF / 2, + "Implementation detail: the length of the message is limited"); + + QFETCH(bool, detached); + QFETCH(bool, useVfork); + + QProcess process; + if (useVfork) + process.setUnixProcessParameters(QProcess::UnixProcessFlag::UseVFork); + process.setChildProcessModifier([&process]() { + process.failChildProcessModifier(failureMsg, EPERM); + }); + process.setProgram("testProcessNormal/testProcessNormal"); + + if (detached) { + qint64 pid; + QVERIFY(!process.startDetached(&pid)); + QCOMPARE(pid, -1); + } else { + process.start(); + QVERIFY(!process.waitForStarted(5000)); + } + + QString errMsg = process.errorString(); + QVERIFY2(errMsg.startsWith("Child process modifier reported error: "_L1 + failureMsg), + qPrintable(errMsg)); + QVERIFY2(errMsg.endsWith(strerror(EPERM)), qPrintable(errMsg)); +} + +void tst_QProcess::throwInChildProcessModifier() +{ +#ifndef __cpp_exceptions + Q_SKIP("Exceptions disabled."); +#else + static constexpr char What[] = "tst_QProcess::throwInChildProcessModifier()::MyException"; + struct MyException : std::exception { + const char *what() const noexcept override { return What; } + }; + QProcess process; + process.setChildProcessModifier([]() { + throw MyException(); + }); + process.setProgram("testProcessNormal/testProcessNormal"); + + process.start(); + QVERIFY(!process.waitForStarted(5000)); + QCOMPARE(process.state(), QProcess::NotRunning); + QCOMPARE(process.error(), QProcess::FailedToStart); + QVERIFY2(process.errorString().contains("Child process modifier threw an exception"), + qPrintable(process.errorString())); + QVERIFY2(process.errorString().contains(What), + qPrintable(process.errorString())); + + // try again, to ensure QProcess internal state wasn't corrupted + process.start(); + QVERIFY(!process.waitForStarted(5000)); + QCOMPARE(process.state(), QProcess::NotRunning); + QCOMPARE(process.error(), QProcess::FailedToStart); + QVERIFY2(process.errorString().contains("Child process modifier threw an exception"), + qPrintable(process.errorString())); + QVERIFY2(process.errorString().contains(What), + qPrintable(process.errorString())); +#endif +} + +void tst_QProcess::terminateInChildProcessModifier_data() +{ + using F = std::function<void(void)>; + QTest::addColumn<F>("function"); + QTest::addColumn<QProcess::ExitStatus>("exitStatus"); + QTest::addColumn<bool>("stderrIsEmpty"); + + QTest::newRow("_exit") << F([]() { _exit(0); }) << QProcess::NormalExit << true; + QTest::newRow("abort") << F(std::abort) << QProcess::CrashExit << true; + QTest::newRow("sigkill") << F([]() { raise(SIGKILL); }) << QProcess::CrashExit << true; + QTest::newRow("terminate") << F(std::terminate) << QProcess::CrashExit + << (std::get_terminate() == std::abort); + QTest::newRow("crash") << F([]() { tst_QProcessCrash::crash(); }) << QProcess::CrashExit << true; +} + +void tst_QProcess::terminateInChildProcessModifier() +{ + QFETCH(std::function<void(void)>, function); + QFETCH(QProcess::ExitStatus, exitStatus); + QFETCH(bool, stderrIsEmpty); + + // temporarily disable QTest's crash logger + DisableCrashLogger disableCrashLogging; + + // testForwardingHelper prints to both stdout and stderr, so if we fail to + // fail we should be able to tell too + QProcess process; + process.setChildProcessModifier(function); + process.setProgram("testForwardingHelper/testForwardingHelper"); + process.setArguments({ "/dev/null" }); + + // temporarily disable QTest's crash logger while starting the child process + { + DisableCrashLogger d; + process.start(); + } + + QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString())); + QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString())); + QCOMPARE(process.exitStatus(), exitStatus); + QCOMPARE(process.readAllStandardOutput(), QByteArray()); + + // some environments print extra stuff to stderr when we crash +#ifndef Q_OS_QNX + if (!QTestPrivate::isRunningArmOnX86()) { + QByteArray standardError = process.readAllStandardError(); + QVERIFY2(standardError.isEmpty() == stderrIsEmpty, + "stderr was: " + standardError); + } +#endif +} + +void tst_QProcess::raiseInChildProcessModifier() +{ +#ifdef QT_BUILD_INTERNAL + // This is similar to the above, but knowing that raise() doesn't unblock + // signals, unlike abort(), this implies that + // 1) the raise() in the child modifier will not run our handler + // 2) the write() to stdout after that will run + // 3) QProcess resets the signal handlers to the defaults, then unblocks + // 4) at that point, the signal will be delivered to the child, but our + // handler is no longer active so there'll be no write() to stderr + // + // Note for maintenance: if in the future this test causes the parent + // process to die with SIGUSR1, it means the C library is buggy and is + // using a cached PID in the child process after vfork(). + if (!QT_PREPEND_NAMESPACE(_qprocessUsingVfork())) + QSKIP("QProcess will only block Unix signals when using vfork()"); + + // we use SIGUSR1 because QtTest doesn't log it and because its default + // action is termination, not core dumping + struct SigUsr1Handler { + SigUsr1Handler() + { + struct sigaction sa = {}; + sa.sa_flags = SA_RESETHAND; + sa.sa_handler = [](int) { + static const char msg[] = "SIGUSR1 handler was run"; + (void)write(STDERR_FILENO, msg, strlen(msg)); + raise(SIGUSR1); // re-raise + }; + sigaction(SIGUSR1, &sa, nullptr); + } + ~SigUsr1Handler() { restore(); } + static void restore() { signal(SIGUSR1, SIG_DFL); } + } sigUsr1Handler; + + QProcess process; + + // QProcess will block signals with UseVFork + process.setUnixProcessParameters(QProcess::UnixProcessFlag::UseVFork | + QProcess::UnixProcessFlag::ResetSignalHandlers); + process.setChildProcessModifier([]() { + raise(SIGUSR1); + ::childProcessModifier(STDOUT_FILENO); + }); + + // testForwardingHelper prints to both stdout and stderr, so if we fail to + // fail we should be able to tell too + process.setProgram("testForwardingHelper/testForwardingHelper"); + process.setArguments({ "/dev/null" }); + + process.start(); + QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString())); + QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString())); + QCOMPARE(process.error(), QProcess::Crashed); + + // ensure the write() from the child modifier DID get run + QCOMPARE(process.readAllStandardOutput(), messageFromChildProcess); + + // some environments print extra stuff to stderr when we crash + if (!QTestPrivate::isRunningArmOnX86()) { + // and write() from the SIGUSR1 handler did not + QCOMPARE(process.readAllStandardError(), QByteArray()); + } +#else + QSKIP("Requires QT_BUILD_INTERNAL symbols"); +#endif +} + +void tst_QProcess::unixProcessParameters_data() +{ + QTest::addColumn<QProcess::UnixProcessParameters>("params"); + QTest::addColumn<QString>("cmd"); + QTest::newRow("defaults") << QProcess::UnixProcessParameters{} << QString(); + + auto addRow = [](const char *cmd, QProcess::UnixProcessFlags flags) { + QProcess::UnixProcessParameters params = {}; + params.flags = flags; + QTest::addRow("%s", cmd) << params << cmd; + }; + using P = QProcess::UnixProcessFlag; + addRow("reset-sighand", P::ResetSignalHandlers); + addRow("ignore-sigpipe", P::IgnoreSigPipe); + addRow("file-descriptors", P::CloseFileDescriptors); + addRow("setsid", P::CreateNewSession); + addRow("reset-ids", P::ResetIds); + + // On FreeBSD, we need to be session leader to disconnect from the CTTY + addRow("noctty", P::DisconnectControllingTerminal | P::CreateNewSession); +} + +void tst_QProcess::unixProcessParameters() +{ + QFETCH(QProcess::UnixProcessParameters, params); + QFETCH(QString, cmd); + + // set up a few things + struct Scope { + int devnull; + struct sigaction old_sigusr1, old_sigpipe; + Scope() + { + int fd = open("/dev/null", O_RDONLY); + devnull = fcntl(fd, F_DUPFD, 100); + close(fd); + + // we ignore SIGUSR1 and reset SIGPIPE to Terminate + struct sigaction act = {}; + sigemptyset(&act.sa_mask); + act.sa_handler = SIG_IGN; + sigaction(SIGUSR1, &act, &old_sigusr1); + act.sa_handler = SIG_DFL; + sigaction(SIGPIPE, &act, &old_sigpipe); + + // and we block SIGUSR2 + sigset_t *set = &act.sa_mask; // reuse this sigset_t + sigaddset(set, SIGUSR2); + sigprocmask(SIG_BLOCK, set, nullptr); + } + ~Scope() + { + if (devnull != -1) + dismiss(); + } + void dismiss() + { + close(devnull); + sigaction(SIGUSR1, &old_sigusr1, nullptr); + sigaction(SIGPIPE, &old_sigpipe, nullptr); + devnull = -1; + + sigset_t *set = &old_sigusr1.sa_mask; // reuse this sigset_t + sigaddset(set, SIGUSR2); + sigprocmask(SIG_BLOCK, set, nullptr); + } + } scope; + + if (params.flags & QProcess::UnixProcessFlag::ResetIds) { + if (getuid() == geteuid() && getgid() == getegid()) + qInfo("Process has identical real and effective IDs; this test will do nothing"); + } + + if (params.flags & QProcess::UnixProcessFlag::DisconnectControllingTerminal) { + if (int fd = open("/dev/tty", O_RDONLY); fd < 0) { + qInfo("Process has no controlling terminal; this test will do nothing"); + close(fd); + } + } + + QProcess process; + process.setUnixProcessParameters(params); + process.setStandardInputFile(QProcess::nullDevice()); // so we can't mess with SIGPIPE + process.setProgram("testUnixProcessParameters/testUnixProcessParameters"); + process.setArguments({ cmd, QString::number(scope.devnull) }); + process.start(); + QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString())); + QVERIFY(process.waitForFinished(5000)); + + const QString stdErr = process.readAllStandardError(); + QCOMPARE(stdErr, QString()); + QCOMPARE(process.readAll(), QString()); + QCOMPARE(process.exitCode(), 0); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); +} + +void tst_QProcess::impossibleUnixProcessParameters_data() +{ + using P = QProcess::UnixProcessParameters; + QTest::addColumn<P>("params"); + QTest::newRow("setsid") << P{ QProcess::UnixProcessFlag::CreateNewSession }; +} + +void tst_QProcess::impossibleUnixProcessParameters() +{ + QFETCH(QProcess::UnixProcessParameters, params); + + QProcess process; + if (params.flags & QProcess::UnixProcessFlag::CreateNewSession) { + process.setChildProcessModifier([]() { + // double setsid() should cause the second to fail + setsid(); + }); + } + process.setUnixProcessParameters(params); + process.start("testProcessNormal/testProcessNormal"); + + QVERIFY(!process.waitForStarted(5000)); + qDebug() << process.errorString(); +} + +void tst_QProcess::unixProcessParametersAndChildModifier() +{ + static constexpr char message[] = "Message from the handler function\n"; + static_assert(std::char_traits<char>::length(message) <= PIPE_BUF); + QProcess process; + QAtomicInt vforkControl; + int pipes[2]; + + pid_t oldpgid = getpgrp(); + + QVERIFY2(pipe(pipes) == 0, qPrintable(qt_error_string())); + auto pipeGuard0 = qScopeGuard([=] { close(pipes[0]); }); + { + auto pipeGuard1 = qScopeGuard([=] { close(pipes[1]); }); + + // verify that our modifier runs before the parameters are applied + process.setChildProcessModifier([=, &vforkControl] { + const char *pgidmsg = "PGID mismatch. "; + if (getpgrp() != oldpgid) + (void)write(pipes[1], pgidmsg, strlen(pgidmsg)); + (void)write(pipes[1], message, strlen(message)); + vforkControl.storeRelaxed(1); + }); + auto flags = QProcess::UnixProcessFlag::CloseFileDescriptors | + QProcess::UnixProcessFlag::CreateNewSession | + QProcess::UnixProcessFlag::UseVFork; + process.setUnixProcessParameters({ flags }); + process.setProgram("testUnixProcessParameters/testUnixProcessParameters"); + process.setArguments({ "file-descriptors", QString::number(pipes[1]) }); + process.start(); + QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString())); + } // closes the writing end of the pipe + + QVERIFY(process.waitForFinished(5000)); + QCOMPARE(process.readAllStandardError(), QString()); + QCOMPARE(process.readAll(), QString()); + + char buf[2 * sizeof(message)]; + int r = read(pipes[0], buf, sizeof(buf)); + QVERIFY2(r >= 0, qPrintable(qt_error_string())); + QCOMPARE(QByteArrayView(buf, r), message); + + if (haveWorkingVFork) + QVERIFY2(vforkControl.loadRelaxed(), "QProcess doesn't appear to have used vfork()"); +} + +void tst_QProcess::unixProcessParametersOtherFileDescriptors() +{ + constexpr int TargetFileDescriptor = 3; + int fd1 = open("/dev/null", O_RDONLY); + int devnull = fcntl(fd1, F_DUPFD, 100); // instead of F_DUPFD_CLOEXEC + close(fd1); + + auto closeFds = qScopeGuard([&] { + close(devnull); + }); + + QProcess process; + QProcess::UnixProcessParameters params; + params.flags = QProcess::UnixProcessFlag::CloseFileDescriptors + | QProcess::UnixProcessFlag::UseVFork; + params.lowestFileDescriptorToClose = 4; + process.setUnixProcessParameters(params); + process.setChildProcessModifier([devnull, &process]() { + if (dup2(devnull, TargetFileDescriptor) != TargetFileDescriptor) + process.failChildProcessModifier("dup2", errno); + }); + process.setProgram("testUnixProcessParameters/testUnixProcessParameters"); + process.setArguments({ "file-descriptors2", QString::number(TargetFileDescriptor), + QString::number(devnull) }); + process.start(); + + QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString())); + QVERIFY(process.waitForFinished(5000)); + QCOMPARE(process.readAllStandardError(), QString()); + QCOMPARE(process.readAll(), QString()); + QCOMPARE(process.exitCode(), 0); + QCOMPARE(process.exitStatus(), QProcess::NormalExit); +} +#endif + void tst_QProcess::exitCodeTest() { for (int i = 0; i < 255; ++i) { @@ -1498,7 +1988,7 @@ void tst_QProcess::failToStart() // to many processes here will cause test failures later on. #if defined Q_OS_HPUX const int attempts = 15; -#elif defined Q_OS_MAC +#elif defined Q_OS_DARWIN const int attempts = 15; #else const int attempts = 50; @@ -1869,12 +2359,21 @@ void tst_QProcess::setStandardInputFile() QProcess process; QFile file(m_temporaryDir.path() + QLatin1String("/data-sif")); + QSignalSpy stateSpy(&process, &QProcess::stateChanged); + QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred); + QVERIFY(file.open(QIODevice::WriteOnly)); file.write(data, sizeof data); file.close(); process.setStandardInputFile(file.fileName()); process.start("testProcessEcho/testProcessEcho"); + QVERIFY(process.waitForStarted()); + QCOMPARE(errorOccurredSpy.size(), 0); + QCOMPARE(stateSpy.size(), 2); + QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting); + QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::Running); + stateSpy.clear(); QVERIFY(process.waitForFinished()); QCOMPARE(process.exitStatus(), QProcess::NormalExit); @@ -1891,31 +2390,50 @@ void tst_QProcess::setStandardInputFile() QCOMPARE(all.size(), 0); } +void tst_QProcess::setStandardInputFileFailure() +{ + QProcess process; + process.setStandardInputFile(nonExistentFileName); + + QSignalSpy stateSpy(&process, &QProcess::stateChanged); + QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred); + + process.start("testProcessEcho/testProcessEcho"); + QVERIFY(!process.waitForStarted()); + + QCOMPARE(errorOccurredSpy.size(), 1); + QCOMPARE(errorOccurredSpy[0][0].value<QProcess::ProcessError>(), QProcess::FailedToStart); + + QCOMPARE(stateSpy.size(), 2); + QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting); + QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::NotRunning); +} + void tst_QProcess::setStandardOutputFile_data() { - QTest::addColumn<int>("channelToTest"); - QTest::addColumn<int>("_channelMode"); + QTest::addColumn<QProcess::ProcessChannel>("channelToTest"); + QTest::addColumn<QProcess::ProcessChannelMode>("channelMode"); QTest::addColumn<bool>("append"); - QTest::newRow("stdout-truncate") << int(QProcess::StandardOutput) - << int(QProcess::SeparateChannels) + QTest::newRow("stdout-truncate") << QProcess::StandardOutput + << QProcess::SeparateChannels << false; - QTest::newRow("stdout-append") << int(QProcess::StandardOutput) - << int(QProcess::SeparateChannels) + QTest::newRow("stdout-append") << QProcess::StandardOutput + << QProcess::SeparateChannels << true; - QTest::newRow("stderr-truncate") << int(QProcess::StandardError) - << int(QProcess::SeparateChannels) + QTest::newRow("stderr-truncate") << QProcess::StandardError + << QProcess::SeparateChannels << false; - QTest::newRow("stderr-append") << int(QProcess::StandardError) - << int(QProcess::SeparateChannels) + QTest::newRow("stderr-append") << QProcess::StandardError + << QProcess::SeparateChannels << true; - QTest::newRow("merged-truncate") << int(QProcess::StandardOutput) - << int(QProcess::MergedChannels) + QTest::newRow("merged-truncate") << QProcess::StandardOutput + << QProcess::MergedChannels << false; - QTest::newRow("merged-append") << int(QProcess::StandardOutput) - << int(QProcess::MergedChannels) + QTest::newRow("merged-append") << QProcess::StandardOutput + << QProcess::MergedChannels << true; } @@ -1924,11 +2442,10 @@ void tst_QProcess::setStandardOutputFile() static const char data[] = "Original data. "; static const char testdata[] = "Test data."; - QFETCH(int, channelToTest); - QFETCH(int, _channelMode); + QFETCH(QProcess::ProcessChannel, channelToTest); + QFETCH(QProcess::ProcessChannelMode, channelMode); QFETCH(bool, append); - QProcess::ProcessChannelMode channelMode = QProcess::ProcessChannelMode(_channelMode); QIODevice::OpenMode mode = append ? QIODevice::Append : QIODevice::Truncate; // create the destination file with data @@ -1945,7 +2462,17 @@ void tst_QProcess::setStandardOutputFile() else process.setStandardErrorFile(file.fileName(), mode); + QSignalSpy stateSpy(&process, &QProcess::stateChanged); + QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred); + process.start("testProcessEcho2/testProcessEcho2"); + QVERIFY(process.waitForStarted()); + QCOMPARE(errorOccurredSpy.size(), 0); + QCOMPARE(stateSpy.size(), 2); + QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting); + QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::Running); + stateSpy.clear(); + process.write(testdata, sizeof testdata); QVERIFY(process.waitForFinished()); QCOMPARE(process.exitStatus(), QProcess::NormalExit); @@ -1970,6 +2497,34 @@ void tst_QProcess::setStandardOutputFile() QCOMPARE(all.size(), expectedsize); } +void tst_QProcess::setStandardOutputFileFailure() +{ + QFETCH(QProcess::ProcessChannel, channelToTest); + QFETCH(QProcess::ProcessChannelMode, channelMode); + QFETCH(bool, append); + + QIODevice::OpenMode mode = append ? QIODevice::Append : QIODevice::Truncate; + + // run the process + QProcess process; + process.setProcessChannelMode(channelMode); + if (channelToTest == QProcess::StandardOutput) + process.setStandardOutputFile(nonExistentFileName, mode); + else + process.setStandardErrorFile(nonExistentFileName, mode); + + QSignalSpy stateSpy(&process, &QProcess::stateChanged); + QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred); + + process.start("testProcessEcho2/testProcessEcho2"); + QVERIFY(!process.waitForStarted()); + QCOMPARE(errorOccurredSpy.size(), 1); + QCOMPARE(errorOccurredSpy[0][0].value<QProcess::ProcessError>(), QProcess::FailedToStart); + QCOMPARE(stateSpy.size(), 2); + QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting); + QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::NotRunning); +} + void tst_QProcess::setStandardOutputFileNullDevice() { static const char testdata[] = "Test data."; @@ -2245,13 +2800,21 @@ void tst_QProcess::setWorkingDirectory() void tst_QProcess::setNonExistentWorkingDirectory() { QProcess process; - process.setWorkingDirectory("this/directory/should/not/exist/for/sure"); + process.setWorkingDirectory(nonExistentFileName); + + QSignalSpy stateSpy(&process, &QProcess::stateChanged); + QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred); // use absolute path because on Windows, the executable is relative to the parent's CWD // while on Unix with fork it's relative to the child's (with posix_spawn, it could be either). process.start(QFileInfo("testSetWorkingDirectory/testSetWorkingDirectory").absoluteFilePath()); + QVERIFY(!process.waitForFinished()); - QCOMPARE(int(process.error()), int(QProcess::FailedToStart)); + QCOMPARE(errorOccurredSpy.size(), 1); + QCOMPARE(process.error(), QProcess::FailedToStart); + QCOMPARE(stateSpy.size(), 2); + QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting); + QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::NotRunning); #ifdef Q_OS_UNIX QVERIFY2(process.errorString().startsWith("chdir:"), process.errorString().toLocal8Bit()); @@ -2261,7 +2824,9 @@ void tst_QProcess::setNonExistentWorkingDirectory() void tst_QProcess::detachedSetNonExistentWorkingDirectory() { QProcess process; - process.setWorkingDirectory("this/directory/should/not/exist/for/sure"); + process.setWorkingDirectory(nonExistentFileName); + + QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred); // use absolute path because on Windows, the executable is relative to the parent's CWD // while on Unix with fork it's relative to the child's (with posix_spawn, it could be either). @@ -2273,6 +2838,9 @@ void tst_QProcess::detachedSetNonExistentWorkingDirectory() QCOMPARE(process.error(), QProcess::FailedToStart); QVERIFY(process.errorString() != "Unknown error"); + QCOMPARE(errorOccurredSpy.size(), 1); + QCOMPARE(process.error(), QProcess::FailedToStart); + #ifdef Q_OS_UNIX QVERIFY2(process.errorString().startsWith("chdir:"), process.errorString().toLocal8Bit()); #endif @@ -2360,7 +2928,7 @@ public: public slots: void block() { - QThread::sleep(1); + QThread::sleep(std::chrono::seconds{1}); } }; @@ -2412,23 +2980,23 @@ void tst_QProcess::startStopStartStop() //----------------------------------------------------------------------------- void tst_QProcess::startStopStartStopBuffers_data() { - QTest::addColumn<int>("channelMode1"); - QTest::addColumn<int>("channelMode2"); + QTest::addColumn<QProcess::ProcessChannelMode>("channelMode1"); + QTest::addColumn<QProcess::ProcessChannelMode>("channelMode2"); - QTest::newRow("separate-separate") << int(QProcess::SeparateChannels) << int(QProcess::SeparateChannels); - QTest::newRow("separate-merged") << int(QProcess::SeparateChannels) << int(QProcess::MergedChannels); - QTest::newRow("merged-separate") << int(QProcess::MergedChannels) << int(QProcess::SeparateChannels); - QTest::newRow("merged-merged") << int(QProcess::MergedChannels) << int(QProcess::MergedChannels); - QTest::newRow("merged-forwarded") << int(QProcess::MergedChannels) << int(QProcess::ForwardedChannels); + QTest::newRow("separate-separate") << QProcess::SeparateChannels << QProcess::SeparateChannels; + QTest::newRow("separate-merged") << QProcess::SeparateChannels << QProcess::MergedChannels; + QTest::newRow("merged-separate") << QProcess::MergedChannels << QProcess::SeparateChannels; + QTest::newRow("merged-merged") << QProcess::MergedChannels << QProcess::MergedChannels; + QTest::newRow("merged-forwarded") << QProcess::MergedChannels << QProcess::ForwardedChannels; } void tst_QProcess::startStopStartStopBuffers() { - QFETCH(int, channelMode1); - QFETCH(int, channelMode2); + QFETCH(QProcess::ProcessChannelMode, channelMode1); + QFETCH(QProcess::ProcessChannelMode, channelMode2); QProcess process; - process.setProcessChannelMode(QProcess::ProcessChannelMode(channelMode1)); + process.setProcessChannelMode(channelMode1); process.start("testProcessHang/testProcessHang"); QVERIFY2(process.waitForReadyRead(), process.errorString().toLocal8Bit()); if (channelMode1 == QProcess::SeparateChannels || channelMode1 == QProcess::ForwardedOutputChannel) { @@ -2439,14 +3007,18 @@ void tst_QProcess::startStopStartStopBuffers() } // We want to test that the write buffer still has bytes after the child - // exiting. We do that by writing to a child process that never reads. We - // just have to write more data than a pipe can hold, so that even if - // QProcess finds the pipe writable (during waitForFinished() or in the - // QWindowsPipeWriter thread), some data will remain. The worst case I know - // of is Linux, which defaults to 64 kB of buffer. + // exits. We can do that by writing data until the OS stops consuming data, + // indicating that the pipe buffers are full. The initial value of 128 kB + // should make this loop typicall run only once; the worst case I know of + // is Linux, which defaults to 64 kB of buffer. - process.write(QByteArray(128 * 1024, 'a')); - QVERIFY(process.bytesToWrite() > 0); + QByteArray chunk(128 * 1024, 'a'); + do { + process.write(chunk); + QVERIFY(process.bytesToWrite() > 0); + process.waitForBytesWritten(1); + } while (process.bytesToWrite() == 0); + chunk = {}; process.kill(); QVERIFY(process.waitForFinished()); @@ -2454,7 +3026,8 @@ void tst_QProcess::startStopStartStopBuffers() #ifndef Q_OS_WIN // confirm that our buffers are still full // Note: this doesn't work on Windows because our buffers are drained into - // QWindowsPipeWriter before being sent to the child process. + // QWindowsPipeWriter before being sent to the child process and are lost + // in waitForFinished() -> processFinished() -> cleanup(). QVERIFY(process.bytesToWrite() > 0); QVERIFY(process.bytesAvailable() > 0); // channelMode1 is not ForwardedChannels if (channelMode1 == QProcess::SeparateChannels || channelMode1 == QProcess::ForwardedOutputChannel) { @@ -2464,7 +3037,7 @@ void tst_QProcess::startStopStartStopBuffers() } #endif - process.setProcessChannelMode(QProcess::ProcessChannelMode(channelMode2)); + process.setProcessChannelMode(channelMode2); process.start("testProcessEcho2/testProcessEcho2", {}, QIODevice::ReadWrite | QIODevice::Text); // the buffers should now be empty |