Files
utun2/tools/chatgui/tests/test_single_instance.cpp
T

208 lines
11 KiB
C++

/* Настоящие процессы и локальные сокеты: один владелец профиля, ACK, startup-race,
* аварийный выход и показ окна без запуска сетевого ядра/аудиоустройств. */
#include "../src/single_instance.h"
#include "debug_config.h"
#include <QtTest>
#include <QApplication>
#include <QDialog>
#include <QDir>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QLocalSocket>
#include <QProcess>
#include <QTemporaryDir>
#include <QTimer>
#include <QWidget>
#include <memory>
#include <vector>
// Дочерний процесс создаёт окно только после определения роли.
static int peerMain(QApplication& app, const QString& profile) {
SingleInstance instance(profile);
const auto role = instance.start();
if (role == SingleInstance::Error) return 2;
if (role == SingleInstance::Activated) { qInfo("PEER_FORWARDED"); return 0; }
QWidget window;
instance.setWindow(&window);
qInfo("PEER_PRIMARY");
QTimer watch;
QObject::connect(&watch, &QTimer::timeout, &window, [&] {
if (window.isVisible()) { qInfo("PEER_VISIBLE"); watch.stop(); }
});
watch.start(10);
QTimer::singleShot(15000, &app, &QCoreApplication::quit);
return app.exec();
}
// Процесс всегда завершается до удаления временного каталога, включая ранний выход QVERIFY.
struct Peer {
QProcess process;
QByteArray output;
explicit Peer(const QString& profile) {
process.setProcessChannelMode(QProcess::MergedChannels);
process.start(QCoreApplication::applicationFilePath(), {"--peer", profile});
}
~Peer() {
if (process.state() != QProcess::NotRunning) { process.kill(); process.waitForFinished(3000); }
}
bool saw(const QByteArray& marker) { output += process.readAll(); return output.contains(marker); }
};
class InstanceTest : public QObject {
Q_OBJECT
private slots:
void forwardsToExistingProcess() {
QTemporaryDir profile; QVERIFY(profile.isValid());
QString path = profile.filePath(QString::fromUtf8("profile space-кириллица")); QVERIFY(QDir().mkpath(path));
Peer first(path); QTRY_VERIFY(first.saw("PEER_PRIMARY"));
Peer second(path); QTRY_COMPARE(second.process.state(), QProcess::NotRunning);
QCOMPARE(second.process.exitCode(), 0); QVERIFY(second.saw("PEER_FORWARDED"));
QVERIFY(!second.output.contains("PEER_PRIMARY")); QTRY_VERIFY(first.saw("PEER_VISIBLE"));
QVERIFY(!QFileInfo::exists(QDir(path).filePath("vibechat.cfg")));
}
void simultaneousStartup() {
QTemporaryDir profile; QVERIFY(profile.isValid());
std::vector<std::unique_ptr<Peer>> peers;
for (int i = 0; i < 4; i++) peers.push_back(std::make_unique<Peer>(profile.path()));
for (const auto& peer : peers) QTRY_VERIFY(peer->saw("PEER_PRIMARY") || peer->saw("PEER_FORWARDED"));
unsigned primary = 0, forwarded = 0;
for (const auto& peer : peers) {
if (peer->output.contains("PEER_PRIMARY")) { primary++; QTRY_VERIFY(peer->saw("PEER_VISIBLE")); }
else {
QTRY_COMPARE(peer->process.state(), QProcess::NotRunning);
QCOMPARE(peer->process.exitCode(), 0); forwarded++;
}
}
QCOMPARE(primary, 1U); QCOMPARE(forwarded, 3U);
}
void differentProfiles() {
QTemporaryDir a, b; QVERIFY(a.isValid() && b.isValid());
Peer first(a.path()), second(b.path());
QTRY_VERIFY(first.saw("PEER_PRIMARY")); QTRY_VERIFY(second.saw("PEER_PRIMARY"));
QVERIFY(!first.output.contains("PEER_FORWARDED") && !second.output.contains("PEER_FORWARDED"));
}
void recoversAfterCrash() {
QTemporaryDir profile; QVERIFY(profile.isValid());
Peer first(profile.path()); QTRY_VERIFY(first.saw("PEER_PRIMARY"));
first.process.kill(); QVERIFY(first.process.waitForFinished(3000));
Peer replacement(profile.path()); QTRY_VERIFY(replacement.saw("PEER_PRIMARY"));
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning);
QCOMPARE(secondary.process.exitCode(), 0); QTRY_VERIFY(replacement.saw("PEER_VISIBLE"));
}
void queuesActivationUntilWindowExists() {
QTemporaryDir profile; QVERIFY(profile.isValid());
SingleInstance instance(profile.path()); QCOMPARE(instance.start(), SingleInstance::Primary);
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning);
QCOMPARE(secondary.process.exitCode(), 0);
QWidget window; QVERIFY(!window.isVisible());
instance.setWindow(&window); window.showMinimized();
QTRY_VERIFY(window.isVisible() && !window.isMinimized());
}
void waitsForServerStartup() {
QTemporaryDir profile; QVERIFY(profile.isValid());
QLockFile lock(profile.filePath("vibechat.lock")); QVERIFY(lock.tryLock());
SingleInstance identity(profile.path()); QLocalServer server;
QTimer::singleShot(250, &server, [&] { QVERIFY(server.listen(identity.serverName())); });
connect(&server, &QLocalServer::newConnection, &server, [&] {
auto* socket = server.nextPendingConnection(); QVERIFY(socket);
connect(socket, &QLocalSocket::readyRead, socket, [socket, request = QByteArray{}]() mutable {
request += socket->readAll();
if (!request.endsWith('\n')) return;
QCOMPARE(request, QByteArray("ACTIVATE\n"));
QCOMPARE(socket->write("OK\n"), 3LL); socket->disconnectFromServer();
});
});
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning);
QCOMPARE(secondary.process.exitCode(), 0); QVERIFY(secondary.saw("PEER_FORWARDED")); QVERIFY(lock.isLocked());
}
void liveOwnerWithoutServerTimesOut() {
QTemporaryDir profile; QVERIFY(profile.isValid());
QLockFile lock(profile.filePath("vibechat.lock")); QVERIFY(lock.tryLock());
QElapsedTimer time; time.start();
Peer secondary(profile.path()); QTRY_COMPARE_WITH_TIMEOUT(secondary.process.state(), QProcess::NotRunning, 7000);
QCOMPARE(secondary.process.exitCode(), 2); QVERIFY(secondary.saw("did not acknowledge"));
QVERIFY(time.elapsed() < 7000 && lock.isLocked()); QVERIFY(!secondary.output.contains("PEER_PRIMARY"));
}
void rejectsMissingAndInvalidAcknowledgement_data() {
QTest::addColumn<QByteArray>("reply");
QTest::newRow("invalid") << QByteArray("NO\n");
QTest::newRow("disconnected") << QByteArray();
QTest::newRow("truncated") << QByteArray("O");
}
void rejectsMissingAndInvalidAcknowledgement() {
QFETCH(QByteArray, reply);
QTemporaryDir profile; QVERIFY(profile.isValid());
QLockFile lock(profile.filePath("vibechat.lock")); QVERIFY(lock.tryLock());
SingleInstance identity(profile.path()); QLocalServer server; QVERIFY(server.listen(identity.serverName()));
connect(&server, &QLocalServer::newConnection, &server, [&] {
auto* socket = server.nextPendingConnection(); QVERIFY(socket);
connect(socket, &QLocalSocket::readyRead, socket, [socket, reply] {
socket->readAll();
if (!reply.isEmpty()) QCOMPARE(socket->write(reply), qint64(reply.size()));
socket->disconnectFromServer();
});
});
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning);
QCOMPARE(secondary.process.exitCode(), 2); QVERIFY(!secondary.saw("PEER_PRIMARY")); QVERIFY(lock.isLocked());
}
void validatesCommandsAndPartialReads() {
QTemporaryDir profile; QVERIFY(profile.isValid());
SingleInstance instance(profile.path()); QCOMPARE(instance.start(), SingleInstance::Primary);
QWidget window; instance.setWindow(&window);
QLocalSocket bad; bad.connectToServer(instance.serverName()); QVERIFY(bad.waitForConnected(1000));
QCOMPARE(bad.write("BAD\n"), 4LL); QVERIFY(bad.waitForBytesWritten(1000));
QTRY_COMPARE(bad.state(), QLocalSocket::UnconnectedState); QVERIFY(!window.isVisible());
QLocalSocket good; good.connectToServer(instance.serverName()); QVERIFY(good.waitForConnected(1000));
QCOMPARE(good.write("ACT"), 3LL); QVERIFY(good.waitForBytesWritten(1000));
QTest::qWait(30); QVERIFY(!window.isVisible()); QCOMPARE(good.bytesAvailable(), 0LL);
QCOMPARE(good.write("IVATE\n"), 6LL); QVERIFY(good.waitForBytesWritten(1000));
QTRY_VERIFY(window.isVisible()); QTRY_COMPARE(good.bytesAvailable(), 3LL); QCOMPARE(good.readAll(), QByteArray("OK\n"));
}
void missingDirectoryFails() {
QTemporaryDir profile; QVERIFY(profile.isValid());
SingleInstance instance(profile.filePath("missing")); QCOMPARE(instance.start(), SingleInstance::Error);
QVERIFY(instance.errorString().contains("does not exist"));
QVERIFY(!QFileInfo::exists(profile.filePath("missing")));
}
void restoresWindowState() {
QWidget window;
showAndActivateWindow(&window); QVERIFY(window.isVisible()); QVERIFY(!window.isMinimized());
window.setWindowState(Qt::WindowMaximized | Qt::WindowMinimized);
showAndActivateWindow(&window); QVERIFY(window.isVisible()); QVERIFY(window.isMaximized()); QVERIFY(!window.isMinimized());
window.hide(); showAndActivateWindow(&window); QVERIFY(window.isVisible() && window.isMaximized());
}
void activatesModalDialog() {
QTemporaryDir profile; QVERIFY(profile.isValid());
SingleInstance instance(profile.path()); QCOMPARE(instance.start(), SingleInstance::Primary);
QWidget window; instance.setWindow(&window); window.show();
QDialog modal(&window); modal.setModal(true); modal.show();
QTRY_COMPARE(QApplication::activeModalWidget(), &modal);
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning);
QCOMPARE(secondary.process.exitCode(), 0); QVERIFY(modal.isVisible());
QCOMPARE(QApplication::activeModalWidget(), &modal);
}
};
int main(int argc, char** argv) {
QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false);
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
if (app.arguments().size() == 3 && app.arguments()[1] == QStringLiteral("--peer"))
return peerMain(app, app.arguments()[2]);
InstanceTest test;
return QTest::qExec(&test, argc, argv);
}
#include "test_single_instance.moc"