/* Настоящие процессы и локальные сокеты: один владелец профиля, ACK, startup-race, * аварийный выход и показ окна без запуска сетевого ядра/аудиоустройств. */ #include "../src/single_instance.h" #include "debug_config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // Дочерний процесс создаёт окно только после определения роли. 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> peers; for (int i = 0; i < 4; i++) peers.push_back(std::make_unique(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("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"