Skip to content

Commit 8ca0909

Browse files
authored
cleanup: remove CFFI deltachat bindings usage, and consolidate test support with rpc-bindings (#872)
* cleanup: remove CFFI deltachat bindings usage, and consolidate test support with rpc-bindings major simplification: all chatmail fixtures used in the test are now created inside the cmdeploy plugin, and do not inherit anything from other fixture machineries, let alone the legacy deltachat CFFI ones. also fix that pytest report headers show correct chatmail domains under test
1 parent 2c99cc8 commit 8ca0909

7 files changed

Lines changed: 121 additions & 105 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ __pycache__/
44
*$py.class
55
*.swp
66
*qr-*.png
7-
chatmail.ini
7+
chatmail*.ini
88

99

1010
# C extensions

cmdeploy/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies = [
2020
"pytest-xdist",
2121
"execnet",
2222
"imap_tools",
23+
"deltachat-rpc-client",
2324
]
2425

2526
[project.scripts]

cmdeploy/src/cmdeploy/cmdeploy.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import argparse
77
import importlib.resources
8-
import importlib.util
98
import os
109
import pathlib
1110
import shutil
@@ -214,14 +213,8 @@ def test_cmd_options(parser):
214213

215214

216215
def test_cmd(args, out):
217-
"""Run local and online tests for chatmail deployment.
216+
"""Run local and online tests for chatmail deployment."""
218217

219-
This will automatically pip-install 'deltachat' if it's not available.
220-
"""
221-
222-
x = importlib.util.find_spec("deltachat")
223-
if x is None:
224-
out.check_call(f"{sys.executable} -m pip install deltachat")
225218
env = os.environ.copy()
226219
if args.ssh_host:
227220
env["CHATMAIL_SSH"] = args.ssh_host

cmdeploy/src/cmdeploy/tests/online/benchmark.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def test_ping_pong(self, benchmark, cmfactory):
4242

4343
def dc_ping_pong():
4444
chat.send_text("ping")
45-
msg = ac2._evtracker.wait_next_incoming_message()
46-
msg.chat.send_text("pong")
47-
ac1._evtracker.wait_next_incoming_message()
45+
msg = ac2.wait_for_incoming_msg()
46+
msg.get_snapshot().chat.send_text("pong")
47+
ac1.wait_for_incoming_msg()
4848

4949
benchmark(dc_ping_pong, 5)
5050

@@ -56,6 +56,6 @@ def dc_send_10_receive_10():
5656
for i in range(10):
5757
chat.send_text(f"hello {i}")
5858
for i in range(10):
59-
ac2._evtracker.wait_next_incoming_message()
59+
ac2.wait_for_incoming_msg()
6060

6161
benchmark(dc_send_10_receive_10, 5, cooldown="auto")

cmdeploy/src/cmdeploy/tests/online/test_1_basic.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,8 @@ def test_remote(remote, imap_or_smtp):
8686

8787

8888
def test_use_two_chatmailservers(cmfactory, maildomain2):
89-
ac1 = cmfactory.new_online_configuring_account(cache=False)
90-
cmfactory.switch_maildomain(maildomain2)
91-
ac2 = cmfactory.new_online_configuring_account(cache=False)
92-
cmfactory.bring_accounts_online()
89+
ac1 = cmfactory.get_online_account()
90+
ac2 = cmfactory.get_online_account(domain=maildomain2)
9391
cmfactory.get_accepted_chat(ac1, ac2)
9492
domain1 = ac1.get_config("addr").split("@")[1]
9593
domain2 = ac2.get_config("addr").split("@")[1]

cmdeploy/src/cmdeploy/tests/online/test_2_deltachat.py

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ def test_one_on_one(self, cmfactory, lp):
6262
chat.send_text("message0")
6363

6464
lp.sec("wait for ac2 to receive message")
65-
msg2 = ac2._evtracker.wait_next_incoming_message()
66-
assert msg2.text == "message0"
65+
msg2 = ac2.wait_for_incoming_msg()
66+
assert msg2.get_snapshot().text == "message0"
6767

6868
def test_exceed_quota(
6969
self, cmfactory, lp, tmpdir, remote, chatmail_config, sshdomain
@@ -110,26 +110,22 @@ def parse_size_limit(limit: str) -> int:
110110
return
111111

112112
def test_securejoin(self, cmfactory, lp, maildomain2):
113-
ac1 = cmfactory.new_online_configuring_account(cache=False)
114-
cmfactory.switch_maildomain(maildomain2)
115-
ac2 = cmfactory.new_online_configuring_account(cache=False)
116-
cmfactory.bring_accounts_online()
113+
ac1 = cmfactory.get_online_account()
114+
ac2 = cmfactory.get_online_account(domain=maildomain2)
117115

118116
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
119-
qr = ac1.get_setup_contact_qr()
117+
qr = ac1.get_qr_code()
120118

121119
lp.sec("ac2: start QR-code based setup contact protocol")
122-
ch = ac2.qr_setup_contact(qr)
120+
ch = ac2.secure_join(qr)
123121
assert ch.id >= 10
124-
ac1._evtracker.wait_securejoin_inviter_progress(1000)
122+
ac1.wait_for_securejoin_inviter_success()
125123

126124
def test_dkim_header_stripped(self, cmfactory, maildomain2, lp, imap_mailbox):
127125
"""Test that if a DC address receives a message, it has no
128126
DKIM-Signature and Authentication-Results headers."""
129-
ac1 = cmfactory.new_online_configuring_account(cache=False)
130-
cmfactory.switch_maildomain(maildomain2)
131-
ac2 = cmfactory.new_online_configuring_account(cache=False)
132-
cmfactory.bring_accounts_online()
127+
ac1 = cmfactory.get_online_account()
128+
ac2 = cmfactory.get_online_account(domain=maildomain2)
133129
chat = cmfactory.get_accepted_chat(ac1, imap_mailbox.dc_ac)
134130
chat.send_text("message0")
135131
chat2 = cmfactory.get_accepted_chat(ac2, imap_mailbox.dc_ac)
@@ -146,29 +142,28 @@ def test_dkim_header_stripped(self, cmfactory, maildomain2, lp, imap_mailbox):
146142
assert "dkim-signature" not in msg.headers
147143

148144
def test_read_receipts_between_instances(self, cmfactory, lp, maildomain2):
149-
ac1 = cmfactory.new_online_configuring_account(cache=False)
150-
cmfactory.switch_maildomain(maildomain2)
151-
ac2 = cmfactory.new_online_configuring_account(cache=False)
152-
cmfactory.bring_accounts_online()
145+
ac1 = cmfactory.get_online_account()
146+
ac2 = cmfactory.get_online_account(domain=maildomain2)
153147

154148
lp.sec("setup encrypted comms between ac1 and ac2 on different instances")
155-
qr = ac1.get_setup_contact_qr()
156-
ch = ac2.qr_setup_contact(qr)
149+
qr = ac1.get_qr_code()
150+
ch = ac2.secure_join(qr)
157151
assert ch.id >= 10
158-
ac1._evtracker.wait_securejoin_inviter_progress(1000)
152+
ac1.wait_for_securejoin_inviter_success()
159153

160154
lp.sec("ac1 sends a message and ac2 marks it as seen")
161155
chat = ac1.create_chat(ac2)
162156
msg = chat.send_text("hi")
163-
m = ac2._evtracker.wait_next_incoming_message()
157+
m = ac2.wait_for_incoming_msg()
164158
m.mark_seen()
165159
# we can only indirectly wait for mark-seen to cause an smtp-error
166160
lp.sec("try to wait for markseen to complete and check error states")
167161
deadline = time.time() + 3.1
168162
while time.time() < deadline:
169-
msgs = m.chat.get_messages()
163+
m_snap = m.get_snapshot()
164+
msgs = m_snap.chat.get_messages()
170165
for msg in msgs:
171-
assert "error" not in m.get_message_info()
166+
assert "error" not in m.get_info()
172167
time.sleep(1)
173168

174169

@@ -180,7 +175,7 @@ def test_hide_senders_ip_address(cmfactory, ssl_context):
180175
chat = cmfactory.get_accepted_chat(user1, user2)
181176

182177
chat.send_text("testing submission header cleanup")
183-
user2._evtracker.wait_next_incoming_message()
178+
user2.wait_for_incoming_msg()
184179
addr = user2.get_config("addr")
185180
host = addr.split("@")[1]
186181
pw = user2.get_config("mail_pw")

cmdeploy/src/cmdeploy/tests/plugin.py

Lines changed: 93 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import imaplib
2-
import io
32
import itertools
43
import os
54
import random
@@ -35,17 +34,24 @@ def pytest_runtest_setup(item):
3534
pytest.skip("skipping slow test, use --slow to run")
3635

3736

38-
@pytest.fixture(scope="session")
39-
def chatmail_config(pytestconfig):
40-
current = basedir = Path().resolve()
37+
def _get_chatmail_config():
38+
current = Path().resolve()
4139
while 1:
4240
path = current.joinpath("chatmail.ini").resolve()
4341
if path.exists():
44-
return read_config(path)
42+
return read_config(path), path
4543
if current == current.parent:
4644
break
4745
current = current.parent
46+
return None, None
4847

48+
49+
@pytest.fixture(scope="session")
50+
def chatmail_config(pytestconfig):
51+
config, path = _get_chatmail_config()
52+
if config:
53+
return config
54+
basedir = Path().resolve()
4955
pytest.skip(f"no chatmail.ini file found in {basedir} or parent dirs")
5056

5157

@@ -73,10 +79,17 @@ def sshdomain2(maildomain2):
7379

7480

7581
def pytest_report_header():
76-
domain = os.environ.get("CHATMAIL_DOMAIN")
77-
if domain:
78-
text = f"chatmail test instance: {domain}"
79-
return ["-" * len(text), text, "-" * len(text)]
82+
config, path = _get_chatmail_config()
83+
domain2 = os.environ.get("CHATMAIL_DOMAIN2", "NOT SET")
84+
domain = config.mail_domain if config else "NOT SET"
85+
path = path if path else "NOT SET"
86+
87+
lines = [
88+
f"chatmail.ini {domain} location: {path}",
89+
f"chatmail2: {domain2}",
90+
]
91+
sep = "-" * max(map(len, lines))
92+
return [sep, *lines, sep]
8093

8194

8295
@pytest.fixture
@@ -283,78 +296,94 @@ def gen(domain=None):
283296

284297

285298
#
286-
# Delta Chat testplugin re-use
299+
# Delta Chat RPC-based test support
287300
# use the cmfactory fixture to get chatmail instance accounts
288301
#
289302

303+
from deltachat_rpc_client import DeltaChat, Rpc
290304

291-
class ChatmailTestProcess:
292-
"""Provider for chatmail instance accounts as used by deltachat.testplugin.acfactory"""
293305

294-
def __init__(self, pytestconfig, maildomain, gencreds, chatmail_config):
295-
self.pytestconfig = pytestconfig
296-
self.maildomain = maildomain
297-
assert "." in self.maildomain, maildomain
306+
class ChatmailACFactory:
307+
"""RPC-based account factory for chatmail testing."""
308+
309+
def __init__(self, rpc, maildomain, gencreds, chatmail_config):
310+
self.dc = DeltaChat(rpc)
311+
self.rpc = rpc
312+
self._maildomain = maildomain
298313
self.gencreds = gencreds
299314
self.chatmail_config = chatmail_config
300-
self._addr2files = {}
301315

302-
def get_liveconfig_producer(self):
303-
while 1:
304-
user, password = self.gencreds(self.maildomain)
305-
config = {
306-
"addr": user,
307-
"mail_pw": password,
308-
}
309-
# speed up account configuration
310-
config["mail_server"] = self.maildomain
311-
config["send_server"] = self.maildomain
312-
if self.chatmail_config.tls_cert_mode == "self":
313-
# Accept self-signed TLS certificates
314-
config["imap_certificate_checks"] = "3"
315-
yield config
316-
317-
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
318-
pass
319-
320-
def cache_maybe_store_configured_db_files(self, acc):
321-
pass
316+
def _make_transport(self, domain):
317+
"""Build a transport config dict for the given domain."""
318+
addr, password = self.gencreds(domain)
319+
transport = {
320+
"addr": addr,
321+
"password": password,
322+
# Setting server explicitly skips requesting autoconfig XML,
323+
# see https://datatracker.ietf.org/doc/draft-ietf-mailmaint-autoconfig/
324+
"imapServer": domain,
325+
"smtpServer": domain,
326+
}
327+
if self.chatmail_config.tls_cert_mode == "self":
328+
transport["certificateChecks"] = "acceptInvalidCertificates"
329+
return transport
330+
331+
def get_online_account(self, domain=None):
332+
"""Create, configure and bring online a single account."""
333+
return self.get_online_accounts(1, domain)[0]
334+
335+
def get_online_accounts(self, num, domain=None):
336+
"""Create multiple online accounts in parallel."""
337+
domain = domain or self._maildomain
338+
futures = []
339+
accounts = []
340+
for _ in range(num):
341+
account = self.dc.add_account()
342+
future = account.add_or_update_transport.future(
343+
self._make_transport(domain)
344+
)
345+
futures.append(future)
322346

347+
# ensure messages stay in INBOX so that they can be
348+
# concurrently fetched via extra IMAP connections during tests
349+
account.set_config("delete_server_after", "10")
350+
accounts.append(account)
323351

324-
@pytest.fixture
325-
def cmfactory(request, gencreds, tmpdir, maildomain, chatmail_config):
326-
# cloned from deltachat.testplugin.amfactory
327-
pytest.importorskip("deltachat")
328-
from deltachat.testplugin import ACFactory
352+
for future in futures:
353+
future()
329354

330-
testproc = ChatmailTestProcess(
331-
request.config, maildomain, gencreds, chatmail_config
332-
)
355+
for account in accounts:
356+
account.bring_online()
357+
return accounts
333358

334-
class Data:
335-
def read_path(self, path):
336-
return
359+
def get_accepted_chat(self, ac1, ac2):
360+
"""Create a 1:1 chat between ac1 and ac2 accepted on both sides."""
361+
ac2.create_chat(ac1)
362+
return ac1.create_chat(ac2)
337363

338-
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testproc, data=Data())
339364

340-
# Skip upstream's init_imap to prevent extra imap connections not
341-
# needed for relay testing
342-
am._acsetup.init_imap = lambda acc: None
365+
@pytest.fixture(scope="session")
366+
def rpc(tmp_path_factory):
367+
"""Start a deltachat-rpc-server process for the test session."""
343368

344-
# nb. a bit hacky
345-
# would probably be better if deltachat's test machinery grows native support
346-
def switch_maildomain(maildomain2):
347-
am.testprocess.maildomain = maildomain2
369+
# NB: accounts_dir must NOT already exist as directory --
370+
# core-rust only creates accounts.toml if the dir doesn't exist yet.
371+
accounts_dir = str(tmp_path_factory.mktemp("dc") / "accounts")
372+
rpc = Rpc(accounts_dir=accounts_dir)
373+
rpc.start()
374+
yield rpc
375+
rpc.close()
348376

349-
am.switch_maildomain = switch_maildomain
350377

351-
yield am
352-
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
353-
if testproc.pytestconfig.getoption("--extra-info"):
354-
logfile = io.StringIO()
355-
am.dump_imap_summary(logfile=logfile)
356-
print(logfile.getvalue())
357-
# request.node.add_report_section("call", "imap-server-state", s)
378+
@pytest.fixture
379+
def cmfactory(rpc, gencreds, maildomain, chatmail_config):
380+
"""Return a ChatmailACFactory for creating online Delta Chat accounts."""
381+
return ChatmailACFactory(
382+
rpc=rpc,
383+
maildomain=maildomain,
384+
gencreds=gencreds,
385+
chatmail_config=chatmail_config,
386+
)
358387

359388

360389
@pytest.fixture

0 commit comments

Comments
 (0)