diff options
Diffstat (limited to 'tests/auto/corelib/io/qprocess/tst_qprocess.cpp')
-rw-r--r-- | tests/auto/corelib/io/qprocess/tst_qprocess.cpp | 952 |
1 files changed, 803 insertions, 149 deletions
diff --git a/tests/auto/corelib/io/qprocess/tst_qprocess.cpp b/tests/auto/corelib/io/qprocess/tst_qprocess.cpp index 035c5941c2..5f35732979 100644 --- a/tests/auto/corelib/io/qprocess/tst_qprocess.cpp +++ b/tests/auto/corelib/io/qprocess/tst_qprocess.cpp @@ -1,31 +1,6 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Copyright (C) 2020 Intel Corporation. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2022 Intel Corporation. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QTest> #include <QTestEventLoop> @@ -40,15 +15,23 @@ #include <QtCore/QRegularExpression> #include <QtCore/QDebug> #include <QtCore/QMetaType> +#include <QtCore/QScopeGuard> #include <QtNetwork/QHostInfo> #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 @@ -64,8 +47,8 @@ private slots: void getSetCheck(); void constructing(); void simpleStart(); - void setChildProcessModifier(); void startCommand(); + void startCommandEmptyString(); void startWithOpen(); void startWithOldOpen(); void execute(); @@ -106,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(); @@ -133,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(); @@ -150,6 +152,8 @@ private slots: void startStopStartStopBuffers(); void processEventsInAReadyReadSlot_data(); void processEventsInAReadyReadSlot(); + void startFromCurrentWorkingDir_data(); + void startFromCurrentWorkingDir(); // keep these at the end, since they use lots of processes and sometimes // caused obscure failures to occur in tests that followed them (esp. on the Mac) @@ -168,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() @@ -264,50 +280,12 @@ void tst_QProcess::simpleStart() process.reset(); - QCOMPARE(spy.count(), 3); + QCOMPARE(spy.size(), 3); QCOMPARE(qvariant_cast<QProcess::ProcessState>(spy.at(0).at(0)), QProcess::Starting); QCOMPARE(qvariant_cast<QProcess::ProcessState>(spy.at(1).at(0)), QProcess::Running); 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; @@ -322,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; @@ -375,9 +372,7 @@ void tst_QProcess::readFromProcess() { QProcess *process = qobject_cast<QProcess *>(sender()); QVERIFY(process); - int lines = 0; while (process->canReadLine()) { - ++lines; process->readLine(); } } @@ -401,10 +396,10 @@ void tst_QProcess::crashTest() QVERIFY(process->waitForFinished(30000)); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); QCOMPARE(*static_cast<const QProcess::ProcessError *>(spy.at(0).at(0).constData()), QProcess::Crashed); - QCOMPARE(spy2.count(), 1); + QCOMPARE(spy2.size(), 1); QCOMPARE(*static_cast<const QProcess::ExitStatus *>(spy2.at(0).at(1).constData()), QProcess::CrashExit); QCOMPARE(process->exitStatus(), QProcess::CrashExit); @@ -412,7 +407,7 @@ void tst_QProcess::crashTest() // delete process; process.reset(); - QCOMPARE(stateSpy.count(), 3); + QCOMPARE(stateSpy.size(), 3); QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(0).at(0)), QProcess::Starting); QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(1).at(0)), QProcess::Running); QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(2).at(0)), QProcess::NotRunning); @@ -439,10 +434,10 @@ void tst_QProcess::crashTest2() if (QTestEventLoop::instance().timeout()) QFAIL("Failed to detect crash : operation timed out"); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); QCOMPARE(*static_cast<const QProcess::ProcessError *>(spy.at(0).at(0).constData()), QProcess::Crashed); - QCOMPARE(spy2.count(), 1); + QCOMPARE(spy2.size(), 1); QCOMPARE(*static_cast<const QProcess::ExitStatus *>(spy2.at(0).at(1).constData()), QProcess::CrashExit); QCOMPARE(process.exitStatus(), QProcess::CrashExit); @@ -548,9 +543,9 @@ void tst_QProcess::echoTest2() break; } - QVERIFY(spy0.count() > 0); - QVERIFY(spy1.count() > 0); - QVERIFY(spy2.count() > 0); + QVERIFY(spy0.size() > 0); + QVERIFY(spy1.size() > 0); + QVERIFY(spy2.size() > 0); QCOMPARE(process.readAllStandardOutput(), QByteArray("Hello")); QCOMPARE(process.readAllStandardError(), QByteArray("Hello")); @@ -651,8 +646,8 @@ void tst_QProcess::exitStatus() QFETCH(QStringList, processList); QFETCH(QList<QProcess::ExitStatus>, exitStatus); - QCOMPARE(exitStatus.count(), processList.count()); - for (int i = 0; i < processList.count(); ++i) { + QCOMPARE(exitStatus.size(), processList.size()); + for (int i = 0; i < processList.size(); ++i) { process.start(processList.at(i)); QVERIFY(process.waitForStarted(5000)); QVERIFY(process.waitForFinished(30000)); @@ -705,7 +700,7 @@ void tst_QProcess::readTimeoutAndThenCrash() QVERIFY(process.waitForFinished(5000)); QCOMPARE(process.state(), QProcess::NotRunning); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); QCOMPARE(*static_cast<const QProcess::ProcessError *>(spy.at(0).at(0).constData()), QProcess::Crashed); } @@ -882,7 +877,7 @@ void tst_QProcess::emitReadyReadOnlyWhenNewDataArrives() proc.start("testProcessEcho/testProcessEcho"); - QCOMPARE(spy.count(), 0); + QCOMPARE(spy.size(), 0); proc.write("A"); @@ -890,7 +885,7 @@ void tst_QProcess::emitReadyReadOnlyWhenNewDataArrives() if (QTestEventLoop::instance().timeout()) QFAIL("Operation timed out"); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); QTestEventLoop::instance().enterLoop(1); QVERIFY(QTestEventLoop::instance().timeout()); @@ -1079,6 +1074,16 @@ void tst_QProcess::mergedChannels() QVERIFY(process.waitForStarted(5000)); + { + QCOMPARE(process.write("abc"), qlonglong(3)); + while (process.bytesAvailable() < 6) + QVERIFY(process.waitForReadyRead(5000)); + QCOMPARE(process.readAllStandardOutput(), QByteArray("aabbcc")); + QTest::ignoreMessage(QtWarningMsg, + "QProcess::readAllStandardError: Called with MergedChannels"); + QCOMPARE(process.readAllStandardError(), QByteArray()); + } + for (int i = 0; i < 100; ++i) { QCOMPARE(process.write("abc"), qlonglong(3)); while (process.bytesAvailable() < 6) @@ -1145,9 +1150,8 @@ void tst_QProcess::forwardedChannels() QVERIFY(process.waitForStarted(5000)); QCOMPARE(process.write("input"), 5); process.closeWriteChannel(); - QVERIFY(process.waitForFinished(5000)); + QVERIFY(process.waitForFinished(40000)); // testForwarding has a 30s wait QCOMPARE(process.exitStatus(), QProcess::NormalExit); - QCOMPARE(process.exitCode(), 0); const char *err; switch (process.exitCode()) { case 0: err = "ok"; break; @@ -1236,6 +1240,13 @@ void tst_QProcess::processInAThread() void tst_QProcess::processesInMultipleThreads() { + if (QTestPrivate::isRunningArmOnX86()) + QSKIP("Test is too slow to run on emulator"); + +#if defined(Q_OS_QNX) + QSKIP("QNX: Large amount of threads is unstable and do not finish in given time"); +#endif + for (int i = 0; i < 10; ++i) { // run from 1 to 10 threads, but run at least some tests // with more threads than the ideal @@ -1244,17 +1255,15 @@ void tst_QProcess::processesInMultipleThreads() threadCount = qMax(threadCount, QThread::idealThreadCount() + 2); QList<TestThread *> threads(threadCount); + QScopeGuard cleanup([&threads]() { qDeleteAll(threads); }); for (int j = 0; j < threadCount; ++j) threads[j] = new TestThread; for (int j = 0; j < threadCount; ++j) threads[j]->start(); - for (int j = 0; j < threadCount; ++j) { + for (int j = 0; j < threadCount; ++j) QVERIFY(threads[j]->wait(10000)); - } - for (int j = 0; j < threadCount; ++j) { + for (int j = 0; j < threadCount; ++j) QCOMPARE(threads[j]->code(), 0); - } - qDeleteAll(threads); } } @@ -1288,7 +1297,7 @@ void tst_QProcess::waitForReadyReadInAReadyReadSlot() QTestEventLoop::instance().enterLoop(30); QVERIFY(!QTestEventLoop::instance().timeout()); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); process.disconnect(); QVERIFY(process.waitForFinished(5000)); @@ -1324,7 +1333,7 @@ void tst_QProcess::waitForBytesWrittenInABytesWrittenSlot() QTestEventLoop::instance().enterLoop(30); QVERIFY(!QTestEventLoop::instance().timeout()); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); process.write("", 1); process.disconnect(); QVERIFY(process.waitForFinished()); @@ -1477,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"; + 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) + write(pipes[1], pgidmsg, strlen(pgidmsg)); + 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) { @@ -1506,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; @@ -1514,7 +1996,7 @@ void tst_QProcess::failToStart() for (int j = 0; j < 8; ++j) { for (int i = 0; i < attempts; ++i) { - QCOMPARE(errorSpy.count(), j * attempts + i); + QCOMPARE(errorSpy.size(), j * attempts + i); process.start("/blurp"); switch (j) { @@ -1538,12 +2020,12 @@ void tst_QProcess::failToStart() } QCOMPARE(process.error(), QProcess::FailedToStart); - QCOMPARE(errorSpy.count(), j * attempts + i + 1); - QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(errorSpy.size(), j * attempts + i + 1); + QCOMPARE(finishedSpy.size(), 0); int it = j * attempts + i + 1; - QCOMPARE(stateSpy.count(), it * 2); + QCOMPARE(stateSpy.size(), it * 2); QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(it * 2 - 2).at(0)), QProcess::Starting); QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(it * 2 - 1).at(0)), QProcess::NotRunning); } @@ -1567,8 +2049,8 @@ void tst_QProcess::failToStartWithWait() process.waitForStarted(); QCOMPARE(process.error(), QProcess::FailedToStart); - QCOMPARE(errorSpy.count(), i + 1); - QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(errorSpy.size(), i + 1); + QCOMPARE(finishedSpy.size(), 0); } } @@ -1594,8 +2076,8 @@ void tst_QProcess::failToStartWithEventLoop() loop.exec(); QCOMPARE(process.error(), QProcess::FailedToStart); - QCOMPARE(errorSpy.count(), i + 1); - QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(errorSpy.size(), i + 1); + QCOMPARE(finishedSpy.size(), 0); } } @@ -1627,7 +2109,7 @@ void tst_QProcess::failToStartEmptyArgs() }; QVERIFY(!process.waitForStarted()); - QCOMPARE(errorSpy.count(), 1); + QCOMPARE(errorSpy.size(), 1); QCOMPARE(process.error(), QProcess::FailedToStart); } @@ -1866,9 +2348,9 @@ void tst_QProcess::waitForReadyReadForNonexistantProcess() QVERIFY(!process.waitForReadyRead()); // used to crash process.start("doesntexist"); QVERIFY(!process.waitForReadyRead()); - QCOMPARE(errorSpy.count(), 1); + QCOMPARE(errorSpy.size(), 1); QCOMPARE(errorSpy.at(0).at(0).toInt(), 0); - QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(finishedSpy.size(), 0); } void tst_QProcess::setStandardInputFile() @@ -1877,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); @@ -1899,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; } @@ -1932,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 @@ -1953,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); @@ -1978,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."; @@ -2253,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()); @@ -2269,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). @@ -2281,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 @@ -2323,7 +2883,7 @@ void tst_QProcess::invalidProgramString() process.start(programString); QCOMPARE(process.error(), QProcess::FailedToStart); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); QVERIFY(!QProcess::startDetached(programString)); } @@ -2342,8 +2902,8 @@ void tst_QProcess::onlyOneStartedSignal() process.start("testProcessNormal/testProcessNormal"); QVERIFY(process.waitForStarted(5000)); QVERIFY(process.waitForFinished(5000)); - QCOMPARE(spyStarted.count(), 1); - QCOMPARE(spyFinished.count(), 1); + QCOMPARE(spyStarted.size(), 1); + QCOMPARE(spyFinished.size(), 1); spyStarted.clear(); spyFinished.clear(); @@ -2352,8 +2912,8 @@ void tst_QProcess::onlyOneStartedSignal() QVERIFY(process.waitForFinished(5000)); QCOMPARE(process.exitStatus(), QProcess::NormalExit); QCOMPARE(process.exitCode(), 0); - QCOMPARE(spyStarted.count(), 1); - QCOMPARE(spyFinished.count(), 1); + QCOMPARE(spyStarted.size(), 1); + QCOMPARE(spyFinished.size(), 1); } class BlockOnReadStdOut : public QObject @@ -2368,7 +2928,7 @@ public: public slots: void block() { - QThread::sleep(1); + QThread::sleep(std::chrono::seconds{1}); } }; @@ -2420,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) { @@ -2447,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()); @@ -2462,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) { @@ -2472,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 @@ -2522,5 +3087,94 @@ void tst_QProcess::processEventsInAReadyReadSlot() QVERIFY(process.waitForFinished()); } +enum class ChdirMode { + None = 0, + InParent, + InChild +}; +Q_DECLARE_METATYPE(ChdirMode) + +void tst_QProcess::startFromCurrentWorkingDir_data() +{ + qRegisterMetaType<ChdirMode>(); + QTest::addColumn<QString>("programPrefix"); + QTest::addColumn<ChdirMode>("chdirMode"); + QTest::addColumn<bool>("success"); + + constexpr bool IsWindows = true +#ifdef Q_OS_UNIX + && false +#endif + ; + + // baseline: trying to execute the directory, this can't possibly succeed! + QTest::newRow("plain-same-cwd") << QString() << ChdirMode::None << false; + + // cross-platform behavior: neither OS searches the setWorkingDirectory() + // dir without "./" + QTest::newRow("plain-child-chdir") << QString() << ChdirMode::InChild << false; + + // cross-platform behavior: both OSes search the parent's CWD with "./" + QTest::newRow("prefixed-parent-chdir") << "./" << ChdirMode::InParent << true; + + // opposite behaviors: Windows searches the parent's CWD and Unix searches + // the child's with "./" + QTest::newRow("prefixed-child-chdir") << "./" << ChdirMode::InChild << !IsWindows; + + // Windows searches the parent's CWD without "./" + QTest::newRow("plain-parent-chdir") << QString() << ChdirMode::InParent << IsWindows; +} + +void tst_QProcess::startFromCurrentWorkingDir() +{ + QFETCH(QString, programPrefix); + QFETCH(ChdirMode, chdirMode); + QFETCH(bool, success); + + QProcess process; + qRegisterMetaType<QProcess::ProcessError>(); + QSignalSpy errorSpy(&process, &QProcess::errorOccurred); + QVERIFY(errorSpy.isValid()); + + // both the dir name and the executable name + const QString target = QStringLiteral("testProcessNormal"); + process.setProgram(programPrefix + target); + +#ifdef Q_OS_UNIX + // Reset PATH, to be sure it doesn't contain . or the empty path. + // We can't do this on Windows because DLLs are searched in PATH + // and Windows always searches "." anyway. + auto restoreEnv = qScopeGuard([old = qgetenv("PATH")] { + qputenv("PATH", old); + }); + qputenv("PATH", "/"); +#endif + + switch (chdirMode) { + case ChdirMode::InParent: { + auto restoreCwd = qScopeGuard([old = QDir::currentPath()] { + QDir::setCurrent(old); + }); + QVERIFY(QDir::setCurrent(target)); + process.start(); + break; + } + case ChdirMode::InChild: + process.setWorkingDirectory(target); + Q_FALLTHROUGH(); + case ChdirMode::None: + process.start(); + break; + } + + QCOMPARE(process.waitForStarted(), success); + QCOMPARE(errorSpy.size(), int(!success)); + if (success) { + QVERIFY(process.waitForFinished()); + } else { + QCOMPARE(process.error(), QProcess::FailedToStart); + } +} + QTEST_MAIN(tst_QProcess) #include "tst_qprocess.moc" |