tests: add ProcessScanner stability test

Add a new QtTest that spawns marker processes and verifies ProcessScanner::scan() matches by env/cmdline, preferEnvironmentMatches behavior, and cancellation. Register the test with ctest.
This commit is contained in:
Marco Allegretti 2026-02-14 17:56:09 +01:00
parent f03eb95b52
commit 5b26b85cc3
2 changed files with 226 additions and 0 deletions

View file

@ -43,3 +43,27 @@ add_test(NAME alakarte_stop_launching_regression_test
set_tests_properties(alakarte_stop_launching_regression_test PROPERTIES
TIMEOUT 60
)
add_executable(alakarte_processscanner_test
processscanner_test.cpp
../src/gamecenter/processscanner.cpp
../src/gamecenter/processscanner.h
)
target_include_directories(alakarte_processscanner_test PRIVATE
${CMAKE_SOURCE_DIR}/src/gamecenter
)
target_link_libraries(alakarte_processscanner_test PRIVATE
Qt6::Core
Qt6::Concurrent
Qt6::Test
)
add_test(NAME alakarte_processscanner_test
COMMAND alakarte_processscanner_test
)
set_tests_properties(alakarte_processscanner_test PROPERTIES
TIMEOUT 20
)

View file

@ -0,0 +1,202 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "processscanner.h"
#include <QElapsedTimer>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <QString>
#include <QUuid>
#include <QtTest>
#include <atomic>
#include <utility>
class ProcessScannerTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void scanFindsEnvAndCmdline();
void preferEnvironmentMatchesFilters();
void cancelledScanReturnsEmpty();
private:
QString m_shell;
QString m_marker;
QProcess m_envProc;
QProcess m_cmdProc;
static bool containsPid(const QList<ProcessScanner::Match> &matches, qint64 pid)
{
for (const auto &m : matches) {
if (m.pid == static_cast<uint>(pid)) {
return true;
}
}
return false;
}
static const ProcessScanner::Match *findByPid(const QList<ProcessScanner::Match> &matches, qint64 pid)
{
for (const auto &m : matches) {
if (m.pid == static_cast<uint>(pid)) {
return &m;
}
}
return nullptr;
}
static bool waitForScan(std::function<bool()> predicate, int timeoutMs)
{
QElapsedTimer t;
t.start();
while (t.elapsed() < timeoutMs) {
if (predicate()) {
return true;
}
QTest::qWait(50);
}
return false;
}
void startProcesses();
void stopProcess(QProcess &p);
};
void ProcessScannerTest::initTestCase()
{
if (!QFileInfo(QStringLiteral("/proc")).isDir()) {
QSKIP("/proc not available");
}
m_shell = QStandardPaths::findExecutable(QStringLiteral("sh"));
if (m_shell.isEmpty()) {
QSKIP("sh not found");
}
m_marker = QUuid::createUuid().toString(QUuid::WithoutBraces);
startProcesses();
}
void ProcessScannerTest::cleanupTestCase()
{
stopProcess(m_envProc);
stopProcess(m_cmdProc);
}
void ProcessScannerTest::startProcesses()
{
const QString script = QStringLiteral("while true; do sleep 60; done # %1").arg(m_marker);
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER"), m_marker);
m_envProc.setProcessEnvironment(env);
m_envProc.start(m_shell, {QStringLiteral("-c"), script});
QVERIFY2(m_envProc.waitForStarted(5000), qPrintable(m_envProc.errorString()));
QVERIFY(m_envProc.processId() > 0);
}
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
m_cmdProc.setProcessEnvironment(env);
m_cmdProc.start(m_shell, {QStringLiteral("-c"), script});
QVERIFY2(m_cmdProc.waitForStarted(5000), qPrintable(m_cmdProc.errorString()));
QVERIFY(m_cmdProc.processId() > 0);
}
}
void ProcessScannerTest::stopProcess(QProcess &p)
{
if (p.state() == QProcess::NotRunning) {
return;
}
p.terminate();
if (!p.waitForFinished(3000)) {
p.kill();
p.waitForFinished(3000);
}
}
void ProcessScannerTest::scanFindsEnvAndCmdline()
{
QVERIFY(m_envProc.processId() > 0);
QVERIFY(m_cmdProc.processId() > 0);
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER")};
opts.envValue = m_marker;
opts.cmdlineSubstring = m_marker;
opts.preferEnvironmentMatches = false;
QList<ProcessScanner::Match> results;
const bool ok = waitForScan(
[&]() {
results = ProcessScanner::scan(opts);
return containsPid(results, m_envProc.processId()) && containsPid(results, m_cmdProc.processId());
},
5000);
QVERIFY(ok);
const auto *envMatch = findByPid(results, m_envProc.processId());
QVERIFY(envMatch);
QVERIFY(envMatch->envMatched);
const auto *cmdMatch = findByPid(results, m_cmdProc.processId());
QVERIFY(cmdMatch);
QVERIFY(!cmdMatch->envMatched);
}
void ProcessScannerTest::preferEnvironmentMatchesFilters()
{
QVERIFY(m_envProc.processId() > 0);
QVERIFY(m_cmdProc.processId() > 0);
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER")};
opts.envValue = m_marker;
opts.cmdlineSubstring = m_marker;
opts.preferEnvironmentMatches = true;
QList<ProcessScanner::Match> results;
const bool ok = waitForScan(
[&]() {
results = ProcessScanner::scan(opts);
return containsPid(results, m_envProc.processId());
},
5000);
QVERIFY(ok);
QVERIFY(containsPid(results, m_envProc.processId()));
QVERIFY(!containsPid(results, m_cmdProc.processId()));
for (const auto &m : std::as_const(results)) {
QVERIFY(m.envMatched);
}
}
void ProcessScannerTest::cancelledScanReturnsEmpty()
{
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER")};
opts.envValue = m_marker;
opts.cmdlineSubstring = m_marker;
opts.preferEnvironmentMatches = false;
const std::atomic_bool cancelled(true);
const QList<ProcessScanner::Match> results = ProcessScanner::scan(opts, cancelled);
QVERIFY(results.isEmpty());
}
QTEST_MAIN(ProcessScannerTest)
#include "processscanner_test.moc"