test: add identify test

fix some bugs found in testing

Signed-off-by: ComixHe <heyuming@deepin.org>
This commit is contained in:
ComixHe 2023-08-15 14:43:34 +08:00 committed by Comix
parent de09f3dbc2
commit a3dd315e33
15 changed files with 250 additions and 85 deletions

View File

@ -4,7 +4,7 @@ Upstream-Contact: UnionTech Software Technology Co., Ltd. <>
Source: https://github.com/linuxdeepin/dde-application-manager Source: https://github.com/linuxdeepin/dde-application-manager
# README & DOC # README & DOC
Files: README.md README.zh_CN.md docs/* Files: README.md README.zh_CN.md docs/* examples/launchApp/README.md
Copyright: UnionTech Software Technology Co., Ltd. Copyright: UnionTech Software Technology Co., Ltd.
License: CC-BY-4.0 License: CC-BY-4.0

View File

@ -17,6 +17,8 @@
#include <thread> #include <thread>
#include "constant.h" #include "constant.h"
namespace {
enum class ExitCode { SystemdError = -3, InvalidInput = -2, InternalError = -1, Done = 0, Waiting = 1 }; enum class ExitCode { SystemdError = -3, InvalidInput = -2, InternalError = -1, Done = 0, Waiting = 1 };
struct JobRemoveResult struct JobRemoveResult
@ -29,7 +31,7 @@ struct JobRemoveResult
using msg_ptr = sd_bus_message *; using msg_ptr = sd_bus_message *;
using bus_ptr = sd_bus *; using bus_ptr = sd_bus *;
static ExitCode fromString(const std::string &str) ExitCode fromString(const std::string &str)
{ {
if (str == "done") { if (str == "done") {
return ExitCode::Done; return ExitCode::Done;
@ -50,7 +52,7 @@ static ExitCode fromString(const std::string &str)
__builtin_unreachable(); __builtin_unreachable();
} }
static ExitCode fromString(const char *str) ExitCode fromString(const char *str)
{ {
if (!str) { if (!str) {
return ExitCode::Waiting; return ExitCode::Waiting;
@ -59,7 +61,7 @@ static ExitCode fromString(const char *str)
return fromString(tmp); return fromString(tmp);
} }
[[noreturn]] static void releaseRes(sd_bus_error &error, msg_ptr &msg, bus_ptr &bus, ExitCode ret) [[noreturn]] void releaseRes(sd_bus_error &error, msg_ptr &msg, bus_ptr &bus, ExitCode ret)
{ {
sd_bus_error_free(&error); sd_bus_error_free(&error);
sd_bus_message_unref(msg); sd_bus_message_unref(msg);
@ -68,7 +70,7 @@ static ExitCode fromString(const char *str)
std::exit(static_cast<int>(ret)); std::exit(static_cast<int>(ret));
} }
static int processExecStart(msg_ptr &msg, const std::deque<std::string_view> &execArgs) int processExecStart(msg_ptr &msg, const std::deque<std::string_view> &execArgs)
{ {
int ret; int ret;
if (ret = sd_bus_message_append(msg, "s", "ExecStart"); ret < 0) { if (ret = sd_bus_message_append(msg, "s", "ExecStart"); ret < 0) {
@ -139,7 +141,7 @@ static int processExecStart(msg_ptr &msg, const std::deque<std::string_view> &ex
return 0; return 0;
} }
static int processKVPair(msg_ptr &msg, const std::map<std::string_view, std::string_view> &props) int processKVPair(msg_ptr &msg, const std::map<std::string_view, std::string_view> &props)
{ {
int ret; int ret;
if (!props.empty()) { if (!props.empty()) {
@ -155,7 +157,7 @@ static int processKVPair(msg_ptr &msg, const std::map<std::string_view, std::str
return 0; return 0;
} }
static std::string cmdParse(msg_ptr &msg, std::deque<std::string_view> &&cmdLines) std::string cmdParse(msg_ptr &msg, std::deque<std::string_view> &&cmdLines)
{ {
std::string serviceName{"internalError"}; std::string serviceName{"internalError"};
std::map<std::string_view, std::string_view> props; std::map<std::string_view, std::string_view> props;
@ -241,6 +243,11 @@ static std::string cmdParse(msg_ptr &msg, std::deque<std::string_view> &&cmdLine
return serviceName; return serviceName;
} }
if (ret = sd_bus_message_append(msg, "(sv)", "Slice", "s", "app.slice"); ret < 0) {
sd_journal_perror("append application slice failed.");
return serviceName;
}
if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_STRUCT, "sv"); ret < 0) { if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_STRUCT, "sv"); ret < 0) {
sd_journal_perror("open struct failed."); sd_journal_perror("open struct failed.");
return serviceName; return serviceName;
@ -300,7 +307,7 @@ int jobRemovedReceiver(sd_bus_message *m, void *userdata, sd_bus_error *ret_erro
return ret; return ret;
} }
static int process_dbus_message(sd_bus *bus) int process_dbus_message(sd_bus *bus)
{ {
int ret; int ret;
ret = sd_bus_process(bus, nullptr); ret = sd_bus_process(bus, nullptr);
@ -322,6 +329,8 @@ static int process_dbus_message(sd_bus *bus)
return ret; return ret;
} }
} // namespace
int main(int argc, const char *argv[]) int main(int argc, const char *argv[])
{ {
sd_bus_error error{SD_BUS_ERROR_NULL}; sd_bus_error error{SD_BUS_ERROR_NULL};

View File

@ -9,12 +9,14 @@
#include "dbus/applicationmanager1service.h" #include "dbus/applicationmanager1service.h"
#include "cgroupsidentifier.h" #include "cgroupsidentifier.h"
static void registerComplexDbusType() namespace {
void registerComplexDbusType()
{ {
qDBusRegisterMetaType<QMap<QString, QDBusUnixFileDescriptor>>(); qDBusRegisterMetaType<QMap<QString, QDBusUnixFileDescriptor>>();
qDBusRegisterMetaType<QMap<uint, QMap<QString, QDBusUnixFileDescriptor>>>(); qDBusRegisterMetaType<QMap<uint, QMap<QString, QDBusUnixFileDescriptor>>>();
qDBusRegisterMetaType<IconMap>(); qDBusRegisterMetaType<IconMap>();
} }
} // namespace
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
@ -22,7 +24,7 @@ int main(int argc, char *argv[])
auto &bus = ApplicationManager1DBus::instance(); auto &bus = ApplicationManager1DBus::instance();
bus.initGlobalServerBus(DBusType::Session); bus.initGlobalServerBus(DBusType::Session);
bus.setDestBus(""); bus.setDestBus();
auto &AMBus = bus.globalServerBus(); auto &AMBus = bus.globalServerBus();
registerComplexDbusType(); registerComplexDbusType();

5
debian/control vendored
View File

@ -3,6 +3,11 @@ Section: devel
Priority: optional Priority: optional
Maintainer: Chen Linxuan <chenlinxuan@uniontech.com> Maintainer: Chen Linxuan <chenlinxuan@uniontech.com>
Build-Depends: Build-Depends:
cmake,
libsystemd-dev,
qt6-base-dev,
libgtest-dev,
pkg-config
Standards-Version: 4.1.3 Standards-Version: 4.1.3
Homepage: https://github.com/linuxdeepin/dde-application-manager Homepage: https://github.com/linuxdeepin/dde-application-manager
Package: dde-application-manager-reborn Package: dde-application-manager-reborn

View File

@ -16,14 +16,13 @@ IdentifyRet CGroupsIdentifier::Identify(pid_t pid)
qWarning() << "open " << AppCgroupPath << "failed: " << AppCgroupFile.errorString(); qWarning() << "open " << AppCgroupPath << "failed: " << AppCgroupFile.errorString();
return {}; return {};
} }
auto appInstance = parseCGroupsPath( auto UnitStr = parseCGroupsPath(QString::fromLocal8Bit(AppCgroupFile.readAll())
AppCgroupFile.readAll().split(':').last().trimmed()); // FIXME: support CGroup version detection and multi-line parsing .split(':', Qt::SkipEmptyParts)
auto appInstanceComponent = appInstance.split('@'); .last()
if (appInstanceComponent.count() != 2) { .trimmed()); // FIXME: support CGroup version detection and multi-line parsing
qWarning() << "Application Instance format is invalid." << appInstanceComponent;
return {}; auto [appId, InstanceId] = processUnitName(UnitStr);
} return {std::move(appId), std::move(InstanceId)};
return {appInstanceComponent.first(), appInstanceComponent.last()};
} }
QString CGroupsIdentifier::parseCGroupsPath(const QString &CGP) noexcept QString CGroupsIdentifier::parseCGroupsPath(const QString &CGP) noexcept
@ -32,8 +31,7 @@ QString CGroupsIdentifier::parseCGroupsPath(const QString &CGP) noexcept
qWarning() << "CGroupPath is empty."; qWarning() << "CGroupPath is empty.";
return {}; return {};
} }
auto unescapedCGP = unescapeString(CGP); auto CGPSlices = CGP.split('/', Qt::SkipEmptyParts);
auto CGPSlices = unescapedCGP.split('/');
if (CGPSlices.first() != "user.slice") { if (CGPSlices.first() != "user.slice") {
qWarning() << "unrecognized process."; qWarning() << "unrecognized process.";
@ -51,7 +49,7 @@ QString CGroupsIdentifier::parseCGroupsPath(const QString &CGP) noexcept
return {}; return {};
} }
auto appInstance = CGPSlices.last().split('.').first(); auto appInstance = CGPSlices.last();
if (appInstance.isEmpty()) { if (appInstance.isEmpty()) {
qWarning() << "get AppId failed."; qWarning() << "get AppId failed.";
return {}; return {};

View File

@ -90,41 +90,6 @@ ApplicationManager1Service::ApplicationManager1Service(std::unique_ptr<Identifie
}); });
} }
QPair<QString, QString> ApplicationManager1Service::processUnitName(const QString &unitName) noexcept
{
QString instanceId;
QString applicationId;
if (unitName.endsWith(".service")) {
auto lastDotIndex = unitName.lastIndexOf('.');
auto app = unitName.sliced(0, lastDotIndex - 1); // remove suffix
if (app.contains('@')) {
auto atIndex = app.indexOf('@');
instanceId = app.sliced(atIndex + 1);
app.remove(atIndex, instanceId.length() + 1);
}
applicationId = app.split('-').last(); // drop launcher if it exists.
} else if (unitName.endsWith(".scope")) {
auto lastDotIndex = unitName.lastIndexOf('.');
auto app = unitName.sliced(0, lastDotIndex - 1);
auto components = app.split('-');
instanceId = components.takeLast();
applicationId = components.takeLast();
} else {
qDebug() << "it's not service or scope:" << unitName << "ignore.";
return {};
}
if (instanceId.isEmpty()) {
instanceId = QUuid::createUuid().toString(QUuid::Id128);
}
return qMakePair(unescapeApplicationId(applicationId), std::move(instanceId));
}
QList<QDBusObjectPath> ApplicationManager1Service::list() const QList<QDBusObjectPath> ApplicationManager1Service::list() const
{ {
return m_applicationList.keys(); return m_applicationList.keys();

View File

@ -62,8 +62,6 @@ private:
std::unique_ptr<Identifier> m_identifier; std::unique_ptr<Identifier> m_identifier;
QScopedPointer<JobManager1Service> m_jobManager{nullptr}; QScopedPointer<JobManager1Service> m_jobManager{nullptr};
QMap<QDBusObjectPath, QSharedPointer<ApplicationService>> m_applicationList; QMap<QDBusObjectPath, QSharedPointer<ApplicationService>> m_applicationList;
static QPair<QString, QString> processUnitName(const QString &serviceName) noexcept;
}; };
#endif #endif

View File

@ -64,7 +64,7 @@ public Q_SLOTS:
private: private:
friend class ApplicationManager1Service; friend class ApplicationManager1Service;
template <typename T> template <typename T>
ApplicationService(T &&source, ApplicationManager1Service *parent) explicit ApplicationService(T &&source, ApplicationManager1Service *parent = nullptr)
: m_parent(parent) : m_parent(parent)
, m_desktopSource(std::forward<T>(source)) , m_desktopSource(std::forward<T>(source))
{ {
@ -84,7 +84,9 @@ private:
m_isPersistence = true; m_isPersistence = true;
sourceFile.setFileName(m_desktopSource.m_file.filePath()); sourceFile.setFileName(m_desktopSource.m_file.filePath());
if (!sourceFile.open(QFile::ExistingOnly | QFile::ReadOnly | QFile::Text)) { if (!sourceFile.open(QFile::ExistingOnly | QFile::ReadOnly | QFile::Text)) {
#ifndef DEBUG_MODE
qCritical() << "desktop file can't open:" << m_desktopSource.m_file.filePath() << sourceFile.errorString(); qCritical() << "desktop file can't open:" << m_desktopSource.m_file.filePath() << sourceFile.errorString();
#endif
return; return;
} }
sourceStream.setDevice(&sourceFile); sourceStream.setDevice(&sourceFile);

View File

@ -18,6 +18,7 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QDBusObjectPath> #include <QDBusObjectPath>
#include <unistd.h> #include <unistd.h>
#include <QUuid>
#include "constant.h" #include "constant.h"
#include "config.h" #include "config.h"
@ -143,7 +144,7 @@ public:
return m_destConnection.value(); return m_destConnection.value();
} }
void setDestBus(const QString &destAddress) void setDestBus(const QString &destAddress = "")
{ {
if (m_destConnection) { if (m_destConnection) {
m_destConnection->disconnectFromBus(ApplicationManagerDestDBusName); m_destConnection->disconnectFromBus(ApplicationManagerDestDBusName);
@ -206,26 +207,6 @@ inline QLocale getUserLocale()
return QLocale::system(); // current use env return QLocale::system(); // current use env
} }
inline QString unescapeString(const QString &input)
{
QRegularExpression regex("\\\\x([0-9A-Fa-f]{2})");
QRegularExpressionMatchIterator it = regex.globalMatch(input);
QString output{input};
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
bool ok;
// Get the hexadecimal value from the match and convert it to a number.
int asciiValue = match.captured(1).toInt(&ok, 16);
if (ok) {
// Convert the ASCII value to a QChar and perform the replacement.
output.replace(match.capturedStart(), match.capturedLength(), QChar(asciiValue));
}
}
return output;
}
inline QString escapeToObjectPath(const QString &str) inline QString escapeToObjectPath(const QString &str)
{ {
if (str.isEmpty()) { if (str.isEmpty()) {
@ -333,4 +314,39 @@ inline QStringList getXDGDataDirs()
return XDGDataDirs; return XDGDataDirs;
} }
inline QPair<QString, QString> processUnitName(const QString &unitName)
{
QString instanceId;
QString applicationId;
if (unitName.endsWith(".service")) {
auto lastDotIndex = unitName.lastIndexOf('.');
auto app = unitName.sliced(0, lastDotIndex); // remove suffix
if (app.contains('@')) {
auto atIndex = app.indexOf('@');
instanceId = app.sliced(atIndex + 1);
app.remove(atIndex, instanceId.length() + 1);
}
applicationId = app.split('-').last(); // drop launcher if it exists.
} else if (unitName.endsWith(".scope")) {
auto lastDotIndex = unitName.lastIndexOf('.');
auto app = unitName.sliced(0, lastDotIndex);
auto components = app.split('-');
instanceId = components.takeLast();
applicationId = components.takeLast();
} else {
qDebug() << "it's not service or scope:" << unitName << "ignore.";
return {};
}
if (instanceId.isEmpty()) {
instanceId = QUuid::createUuid().toString(QUuid::Id128);
}
return qMakePair(unescapeApplicationId(applicationId), std::move(instanceId));
}
#endif #endif

View File

@ -18,7 +18,16 @@ target_link_libraries(${BIN_NAME} PRIVATE
dde_am_static dde_am_static
) )
target_compile_options(${BIN_NAME} PRIVATE "-fno-access-control") target_compile_options(${BIN_NAME} PRIVATE
-fno-access-control
-fsanitize=undefined
-fsanitize=address
)
target_link_options(${BIN_NAME} PRIVATE
-fsanitize=undefined
-fsanitize=address
)
add_test( add_test(
NAME UnitTest NAME UnitTest

View File

@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "dbus/applicationmanager1service.h"
#include "dbus/applicationservice.h"
#include "cgroupsidentifier.h"
#include "constant.h"
#include <gtest/gtest.h>
#include <QDBusConnection>
#include <QSharedPointer>
#include <sys/syscall.h>
#include <QProcess>
#include <thread>
#include <chrono>
#include <QDBusUnixFileDescriptor>
namespace {
int pidfd_open(pid_t pid, uint flags)
{
return syscall(SYS_pidfd_open, pid, flags);
}
} // namespace
class TestApplicationManager : public testing::Test
{
public:
static void SetUpTestCase()
{
auto &bus = ApplicationManager1DBus::instance();
bus.initGlobalServerBus(DBusType::Session);
bus.setDestBus();
m_am = new ApplicationManager1Service{std::make_unique<CGroupsIdentifier>(), bus.globalServerBus()};
DesktopFile file{"/usr/share/applications/test-Application.desktop", "test-Application", 0};
QSharedPointer<ApplicationService> app = QSharedPointer<ApplicationService>::create(std::move(file));
QSharedPointer<InstanceService> instance =
QSharedPointer<InstanceService>::create(InstancePath.path().split('/').last(), ApplicationPath.path(), QString{"/"});
app->m_Instances.insert(InstancePath, instance);
m_am->m_applicationList.insert(ApplicationPath, app);
}
static void TearDownTestCase() { m_am->deleteLater(); }
static inline ApplicationManager1Service *m_am{nullptr};
const static inline QDBusObjectPath ApplicationPath{DDEApplicationManager1ApplicationObjectPath +
QUuid::createUuid().toString(QUuid::Id128)};
const static inline QDBusObjectPath InstancePath{DDEApplicationManager1InstanceObjectPath +
QUuid::createUuid().toString(QUuid::Id128)};
};
TEST_F(TestApplicationManager, list)
{
auto lists = m_am->list();
EXPECT_EQ(lists.first(), ApplicationPath);
}
TEST_F(TestApplicationManager, application)
{
EXPECT_EQ(m_am->Application("test-Application"), ApplicationPath);
}
TEST_F(TestApplicationManager, identifyService)
{
using namespace std::chrono_literals;
// for service unit
auto workingDir = QDir::cleanPath(QDir::current().absolutePath() + QDir::separator() + ".." + QDir::separator() + "tools");
QFile pidFile{workingDir + QDir::separator() + "pid.txt"};
if (pidFile.exists()) {
ASSERT_TRUE(pidFile.remove());
}
QProcess fakeServiceProc;
fakeServiceProc.setWorkingDirectory(workingDir);
auto InstanceId = InstancePath.path().split('/').last();
fakeServiceProc.start("/usr/bin/systemd-run",
{{QString{R"(--unit=app-DDE-test\x2dApplication@%1.service)"}.arg(InstanceId)},
{"--user"},
{QString{"--working-directory=%1"}.arg(workingDir)},
{"--slice=app.slice"},
{"./fake-process.sh"}});
fakeServiceProc.waitForFinished();
if (fakeServiceProc.exitStatus() != QProcess::ExitStatus::NormalExit) {
GTEST_SKIP() << "invoke systemd-run failed.";
}
std::this_thread::sleep_for(100ms);
auto success = pidFile.open(QFile::ReadOnly | QFile::Text | QFile::ExistingOnly);
EXPECT_TRUE(success);
if (!success) {
qWarning() << pidFile.errorString();
fakeServiceProc.terminate();
ASSERT_TRUE(false);
}
bool ok{false};
auto fakePid = pidFile.readLine().toInt(&ok);
ASSERT_TRUE(ok);
auto pidfd = pidfd_open(fakePid, 0);
ASSERT_TRUE(pidfd > 0) << std::strerror(errno);
QDBusObjectPath application;
QDBusObjectPath application_instance;
auto appId = m_am->Identify(QDBusUnixFileDescriptor{pidfd}, application, application_instance);
EXPECT_EQ(appId.toStdString(), QString{"test-Application"}.toStdString());
EXPECT_EQ(application.path().toStdString(), ApplicationPath.path().toStdString());
EXPECT_EQ(application_instance.path().toStdString(), InstancePath.path().toStdString());
close(pidfd);
if (pidFile.exists()) {
pidFile.remove();
}
// for scope unit
ASSERT_TRUE(QProcess::startDetached("/usr/bin/systemd-run",
{{"--scope"},
{QString{R"(--unit=app-DDE-test\x2dApplication-%1.scope)"}.arg(InstanceId)},
{"--user"},
{QString{"--working-directory=%1"}.arg(workingDir)},
{"--slice=app.slice"},
{"./fake-process.sh"},
{"Scope"}},
workingDir));
std::this_thread::sleep_for(100ms);
success = pidFile.open(QFile::ReadOnly | QFile::Text | QFile::ExistingOnly);
EXPECT_TRUE(success);
if (!success) {
qWarning() << pidFile.errorString();
ASSERT_TRUE(false);
}
ok = false;
fakePid = pidFile.readLine().toInt(&ok);
ASSERT_TRUE(ok);
pidfd = pidfd_open(fakePid, 0);
ASSERT_TRUE(pidfd > 0) << std::strerror(errno);
appId = m_am->Identify(QDBusUnixFileDescriptor{pidfd}, application, application_instance);
EXPECT_EQ(appId.toStdString(), QString{"test-Application"}.toStdString());
EXPECT_EQ(application.path().toStdString(), ApplicationPath.path().toStdString());
EXPECT_EQ(application_instance.path().toStdString(), InstancePath.path().toStdString());
close(pidfd);
if (pidFile.exists()) {
pidFile.remove();
}
}

View File

@ -30,8 +30,6 @@ public:
static void TearDownTestCase() { qputenv("XDG_DATA_DIRS", env); } static void TearDownTestCase() { qputenv("XDG_DATA_DIRS", env); }
void SetUp() override {}
void TearDown() override {}
QSharedPointer<DesktopFile> file() { return m_file; } QSharedPointer<DesktopFile> file() { return m_file; }
private: private:

View File

@ -43,5 +43,5 @@ TEST_F(TestJobManager, addJob)
return QVariant::fromValue(true); return QVariant::fromValue(true);
}, },
args); args);
QThread::sleep(1); // force wait QThreadPool::globalInstance()->waitForDone();
} }

9
tools/fake-process.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
echo $$ > pid.txt
sleep 1s
exit 0

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. # SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later