File indexing completed on 2024-04-28 05:50:35

0001 /*
0002     SPDX-FileCopyrightText: 2014 Kurt Hindenburg <kurt.hindenburg@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 // Own
0008 #include "TerminalInterfaceTest.h"
0009 #include "../profile/Profile.h"
0010 #include "../profile/ProfileManager.h"
0011 #include "config-konsole.h"
0012 
0013 // Qt
0014 #include <QDir>
0015 #include <QSignalSpy>
0016 
0017 // KDE
0018 #include <KPluginFactory>
0019 #include <kcoreaddons_version.h>
0020 
0021 // Others
0022 #if defined(Q_OS_LINUX)
0023 #include <unistd.h>
0024 #elif defined(Q_OS_OPENBSD)
0025 #include <kvm.h>
0026 #include <sys/param.h>
0027 #include <sys/sysctl.h>
0028 #endif
0029 
0030 #include <QTest>
0031 
0032 using namespace Konsole;
0033 
0034 /* TerminalInterface found in KParts/kde_terminal_interface.h
0035  *
0036  *  void startProgram(const QString &program,
0037  *                    const QStringList &args)
0038  *  void showShellInDir(const QString &dir)
0039  *  void sendInput(const QString &text)
0040  *  int terminalProcessId()
0041  *  int foregroundProcessId()
0042  *  QString foregroundProcessName()
0043  *  QString currentWorkingDirectory() const
0044  */
0045 
0046 void TerminalInterfaceTest::initTestCase()
0047 {
0048     /* Try to test against build konsolepart, so move directory containing
0049       executable to front of libraryPaths.  KPluginLoader should find the
0050       part first in the build dir over the system installed ones.
0051       I believe the CI installs first and then runs the test so the other
0052       paths can not be removed.
0053     */
0054     const auto libraryPaths = QCoreApplication::libraryPaths();
0055     auto buildPath = libraryPaths.last();
0056     QCoreApplication::removeLibraryPath(buildPath);
0057     // konsolepart.so is in ../autotests/
0058     if (buildPath.endsWith(QStringLiteral("/autotests"))) {
0059         buildPath.chop(10);
0060     }
0061     QCoreApplication::addLibraryPath(buildPath);
0062 }
0063 
0064 // Test with no shell running
0065 void TerminalInterfaceTest::testTerminalInterfaceNoShell()
0066 {
0067     // create a Konsole part and attempt to connect to it
0068     _terminalPart = createPart();
0069     if (_terminalPart == nullptr) {
0070         QFAIL("konsolepart not found.");
0071     }
0072 
0073     TerminalInterface *terminal = qobject_cast<TerminalInterface *>(_terminalPart);
0074     QVERIFY(terminal);
0075 
0076 #if !defined(Q_OS_FREEBSD)
0077     // Skip this for now on FreeBSD
0078     // -1 is current foreground process and name for process 0 is "kernel"
0079 
0080     // Verify results when no shell running
0081     int terminalProcessId = terminal->terminalProcessId();
0082     QCOMPARE(terminalProcessId, 0);
0083     int foregroundProcessId = terminal->foregroundProcessId();
0084     QCOMPARE(foregroundProcessId, -1);
0085     QString foregroundProcessName = terminal->foregroundProcessName();
0086     QCOMPARE(foregroundProcessName, QString());
0087     const QString currentWorkingDirectory = terminal->currentWorkingDirectory();
0088     QCOMPARE(currentWorkingDirectory, QString());
0089 
0090 #endif
0091     delete _terminalPart;
0092 }
0093 
0094 // Test with default shell running
0095 void TerminalInterfaceTest::testTerminalInterface()
0096 {
0097     // Maybe https://bugreports.qt.io/browse/QTBUG-82351 ???
0098     QSKIP("Skipping on CI suse_tumbelweed_qt64", SkipSingle);
0099     return;
0100 
0101     QString currentDirectory;
0102 
0103     // create a Konsole part and attempt to connect to it
0104     _terminalPart = createPart();
0105     if (_terminalPart == nullptr) {
0106         QFAIL("konsolepart not found.");
0107     }
0108 
0109     TerminalInterface *terminal = qobject_cast<TerminalInterface *>(_terminalPart);
0110     QVERIFY(terminal);
0111 
0112     // Start a shell in given directory
0113     terminal->showShellInDir(QDir::home().path());
0114 
0115 // After fa398f56, the CI test failed; also the KF was updated on that build.
0116 // TODO: research this more
0117 #if defined(Q_OS_FREEBSD)
0118     return;
0119 #endif
0120     // Skip this for now on FreeBSD
0121     // -1 is current foreground process and name for process 0 is "kernel"
0122 
0123     int foregroundProcessId = terminal->foregroundProcessId();
0124     QCOMPARE(foregroundProcessId, -1);
0125     QString foregroundProcessName = terminal->foregroundProcessName();
0126     QCOMPARE(foregroundProcessName, QString());
0127 
0128     // terminalProcessId() is the user's default shell
0129     // FIXME: find a way to verify this
0130     int terminalProcessId = terminal->terminalProcessId();
0131 #if defined(Q_OS_LINUX)
0132     const int uid = getuid();
0133     QFile passwdFile(QStringLiteral("/etc/passwd"));
0134     QString defaultExePath;
0135 
0136     passwdFile.open(QIODevice::ReadOnly);
0137 
0138     do {
0139         const QString userData(QString::fromUtf8(passwdFile.readLine()));
0140         const QStringList dataFields(userData.split(QLatin1Char(':')));
0141         if (dataFields.at(2).toInt() == uid) {
0142             defaultExePath = dataFields.at(6);
0143         }
0144     } while (defaultExePath.isEmpty());
0145 
0146     passwdFile.close();
0147 
0148     QFile procExeTarget(QStringLiteral("/proc/%1/exe").arg(terminalProcessId));
0149     if (procExeTarget.exists()) {
0150         QCOMPARE(procExeTarget.symLinkTarget(), defaultExePath.trimmed());
0151     }
0152 #endif
0153 
0154     // Let's try using QSignalSpy
0155     // https://community.kde.org/Guidelines_and_HOWTOs/UnitTests
0156     // QSignalSpy is really a QList of QLists, so we take the first
0157     // list, which corresponds to the arguments for the first signal
0158     // we caught.
0159 
0160     QSignalSpy stateSpy(_terminalPart, SIGNAL(currentDirectoryChanged(QString)));
0161     QVERIFY(stateSpy.isValid());
0162 
0163     // Now we check to make sure we don't have any signals already
0164     QCOMPARE(stateSpy.count(), 0);
0165 
0166     // Let's trigger some signals
0167 
0168     // #1A - Test signal currentDirectoryChanged(QString)
0169     currentDirectory = QStringLiteral("/tmp");
0170     terminal->sendInput(QStringLiteral("cd ") + currentDirectory + QLatin1Char('\n'));
0171     stateSpy.wait(5000);
0172     QCOMPARE(stateSpy.count(), 1);
0173 
0174     // Correct result?
0175     QList<QVariant> firstSignalArgs = stateSpy.takeFirst();
0176 
0177     // Actual: /Users/kurthindenburg
0178     // Expected: /tmp
0179 #if !defined(Q_OS_MACOS)
0180     QString firstSignalState = firstSignalArgs.at(0).toString();
0181     QCOMPARE(firstSignalState, currentDirectory);
0182 
0183     const QString currentWorkingDirectory = terminal->currentWorkingDirectory();
0184     QCOMPARE(currentWorkingDirectory, currentDirectory);
0185 
0186     // #1B - Test signal currentDirectoryChanged(QString)
0187     // Invalid directory - no signal should be emitted
0188     terminal->sendInput(QStringLiteral("cd /usrADADFASDF\n"));
0189     stateSpy.wait(2500);
0190     QCOMPARE(stateSpy.count(), 0);
0191 
0192     // Should be no change since the above cd didn't work
0193     const QString currentWorkingDirectory2 = terminal->currentWorkingDirectory();
0194     QCOMPARE(currentWorkingDirectory2, currentDirectory);
0195 
0196     // Test starting a new program
0197     QString command = QStringLiteral("top");
0198     terminal->sendInput(command + QLatin1Char('\n'));
0199     stateSpy.wait(2500);
0200     foregroundProcessId = terminal->foregroundProcessId();
0201     QVERIFY(foregroundProcessId != -1);
0202 
0203 // Check that pid indeed belongs to a process running 'top'
0204 #if defined(Q_OS_LINUX)
0205     QFile procInfo(QStringLiteral("/proc/%1/stat").arg(foregroundProcessId));
0206     QVERIFY(procInfo.open(QIODevice::ReadOnly));
0207     const QString data(QString::fromUtf8(procInfo.readAll()));
0208     const int nameStartIdx = data.indexOf(QLatin1Char('(')) + 1;
0209     const QString name(data.mid(nameStartIdx, data.lastIndexOf(QLatin1Char(')')) - nameStartIdx));
0210     QCOMPARE(name, command);
0211 #elif defined(Q_OS_SOLARIS)
0212     QFile procExeTarget(QStringLiteral("/proc/%1/execname").arg(foregroundProcessId));
0213     QVERIFY(procExeTarget.open(QIODevice::ReadOnly));
0214     QCOMPARE(QStringLiteral(procExeTarget.readAll()), command);
0215 #elif defined(Q_OS_OPENBSD)
0216     kvm_t *kd = kvm_openfiles(NULL, NULL, NULL, O_RD_ONLY, NULL);
0217     int count;
0218     auto kProcStruct = ::kvm_getprocs(kd, KERN_PROC_PID, foregroundProcessId, &count);
0219     QCOMPARE(count, 1);
0220     QCOMPARE(QStringLiteral(kProcStruct->p_comm), command);
0221     kvm_close(kd);
0222 #endif
0223 
0224     // check that Terminal::foregroundProcessName outputs name of correct command
0225     foregroundProcessName = terminal->foregroundProcessName();
0226     QCOMPARE(foregroundProcessName, command);
0227 
0228     terminal->sendInput(QStringLiteral("q"));
0229     stateSpy.wait(2500);
0230 
0231     // Nothing running in foreground
0232     foregroundProcessId = terminal->foregroundProcessId();
0233     QCOMPARE(foregroundProcessId, -1);
0234     foregroundProcessName = terminal->foregroundProcessName();
0235     QCOMPARE(foregroundProcessName, QString());
0236 #endif
0237 
0238     // Test destroyed()
0239     QSignalSpy destroyedSpy(_terminalPart, SIGNAL(destroyed()));
0240     QVERIFY(destroyedSpy.isValid());
0241 
0242     // Now we check to make sure we don't have any signals already
0243     QCOMPARE(destroyedSpy.count(), 0);
0244 
0245     delete _terminalPart;
0246     QCOMPARE(destroyedSpy.count(), 1);
0247 }
0248 
0249 void TerminalInterfaceTest::testTerminalInterfaceV2()
0250 {
0251     // Use the built-in profile for testing
0252     Profile::Ptr testProfile = ProfileManager::instance()->builtinProfile();
0253 
0254     _terminalPart = createPart();
0255     if (_terminalPart == nullptr) {
0256         QFAIL("konsolepart not found.");
0257     }
0258 
0259     TerminalInterface *terminal = qobject_cast<TerminalInterface *>(_terminalPart);
0260 
0261     QVERIFY(terminal);
0262     QVERIFY(terminal->setCurrentProfile(testProfile->name()));
0263     QCOMPARE(terminal->currentProfileName(), testProfile->name());
0264 
0265     QCOMPARE(terminal->profileProperty(QStringLiteral("Path")), testProfile->path());
0266     QCOMPARE(terminal->profileProperty(QStringLiteral("SilenceSeconds")), testProfile->silenceSeconds());
0267     QCOMPARE(terminal->profileProperty(QStringLiteral("Icon")), testProfile->icon());
0268     QCOMPARE(terminal->profileProperty(QStringLiteral("ShowTerminalSizeHint")), testProfile->showTerminalSizeHint());
0269     QCOMPARE(terminal->profileProperty(QStringLiteral("Environment")), testProfile->environment());
0270     QCOMPARE(terminal->profileProperty(QStringLiteral("BellMode")), testProfile->property<QVariant>(Profile::Property::BellMode));
0271 }
0272 
0273 KParts::Part *TerminalInterfaceTest::createPart()
0274 {
0275     const KPluginMetaData metaData(QStringLiteral("konsolepart"));
0276     Q_ASSERT(metaData.isValid());
0277 
0278     KPluginFactory::Result<KParts::Part> result = KPluginFactory::instantiatePlugin<KParts::Part>(metaData, this);
0279     Q_ASSERT(result);
0280 
0281     return result.plugin;
0282 }
0283 
0284 QTEST_MAIN(TerminalInterfaceTest)
0285 
0286 #include "moc_TerminalInterfaceTest.cpp"