1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
/*
* qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
* Copyright (C) 2026 Danilo M. <danix@danix.xyz>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <QLocalServer>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QtTest>
#include "launchselectors.h"
#include "singleinstance.h"
/// The socket half of item 200. Every case runs against a socket name of its
/// own inside a QTemporaryDir, so nothing here can reach a real running
/// qtmaildir, and two cases cannot collide.
class TestSingleInstance : public QObject
{
Q_OBJECT
private slots:
void init();
void theFirstInstanceBecomesTheServer();
void aSecondInstanceHandsOverItsSelectors();
void aSecondInstanceWithNoSelectorsStillArrives();
void aStaleSocketFileIsReclaimed();
void anUncreatableSocketDoesNotStopStartup();
private:
QTemporaryDir m_dir;
QString m_socketPath;
};
void TestSingleInstance::init()
{
QVERIFY(m_dir.isValid());
// A name per test function, so a socket left behind by one case cannot
// decide the next one's result.
m_socketPath = m_dir.filePath(
QStringLiteral("sock-%1").arg(QTest::currentTestFunction()));
}
void TestSingleInstance::theFirstInstanceBecomesTheServer()
{
SingleInstance first(m_socketPath);
QVERIFY(first.tryBecomeServer());
QVERIFY(first.isServer());
}
void TestSingleInstance::aSecondInstanceHandsOverItsSelectors()
{
// The whole point of the feature: the second process does not open a
// window, it hands its request to the first and exits.
SingleInstance first(m_socketPath);
QVERIFY(first.tryBecomeServer());
QSignalSpy arrived(&first, &SingleInstance::selectorsReceived);
LaunchSelectors selectors;
selectors.account = QStringLiteral("work");
selectors.messageId = QStringLiteral("<abc@example.org>");
SingleInstance second(m_socketPath);
QVERIFY(!second.tryBecomeServer());
QVERIFY(second.sendToRunningInstance(selectors));
QTRY_VERIFY_WITH_TIMEOUT(arrived.count() == 1, 5000);
const auto received =
arrived.first().at(0).value<LaunchSelectors>();
QCOMPARE(received.account, QStringLiteral("work"));
QCOMPARE(received.messageId, QStringLiteral("<abc@example.org>"));
}
void TestSingleInstance::aSecondInstanceWithNoSelectorsStillArrives()
{
// A bare `qtmaildir` against a running instance means "raise yourself".
// That is a real request, so it must arrive rather than being dropped as
// an empty message.
SingleInstance first(m_socketPath);
QVERIFY(first.tryBecomeServer());
QSignalSpy arrived(&first, &SingleInstance::selectorsReceived);
SingleInstance second(m_socketPath);
QVERIFY(!second.tryBecomeServer());
QVERIFY(second.sendToRunningInstance(LaunchSelectors()));
QTRY_VERIFY_WITH_TIMEOUT(arrived.count() == 1, 5000);
QVERIFY(arrived.first().at(0).value<LaunchSelectors>().isEmpty());
}
void TestSingleInstance::aStaleSocketFileIsReclaimed()
{
// A crash or a kill leaves the socket file behind, and listen() then fails
// with AddressInUse on a file nothing is serving. Without recovery the
// application would never start again until someone deleted it by hand.
//
// A plain file at the socket path stands in for what a killed process
// leaves behind: a filesystem entry with no process serving it, so nothing
// answers a connection.
QFile stale(m_socketPath);
QVERIFY(stale.open(QIODevice::WriteOnly));
stale.close();
QVERIFY(QFile::exists(m_socketPath));
SingleInstance fresh(m_socketPath);
QVERIFY2(fresh.tryBecomeServer(),
"a stale socket file must not stop the application starting");
QVERIFY(fresh.isServer());
}
void TestSingleInstance::anUncreatableSocketDoesNotStopStartup()
{
// A read-only state directory must degrade to today's behaviour, a window
// that opens and works, rather than to no mail client at all. The caller
// reads isServer() as false and carries on.
const QString impossible =
m_dir.filePath(QStringLiteral("no/such/directory/sock"));
SingleInstance instance(impossible);
QVERIFY(!instance.tryBecomeServer());
QVERIFY(!instance.isServer());
// And it cannot reach a running instance either, since there is none.
QVERIFY(!instance.sendToRunningInstance(LaunchSelectors()));
}
QTEST_MAIN(TestSingleInstance)
#include "test_singleinstance.moc"
|